From 7133ac787cdd2096e728af3cff2d99220c214041 Mon Sep 17 00:00:00 2001 From: ApiliumDevTeam Date: Mon, 22 Jun 2026 18:54:10 +0200 Subject: [PATCH] fix(graph): share Sled Db between triple store and persistent DAG enable_dag_persistent and sled_with_dag previously called sled::open on the same path the triple store already had open, which deadlocks on the file lock (only on real --db paths; :memory: masked it in tests). Now the DAG tree is opened on the triple store's existing sled::Db via a new SledDagBackend::from_db, reached through a StorageBackend::as_any downcast to SledBackend. Adds a regression test reproducing the lock self-conflict. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/aingle_graph/src/backends/memory.rs | 4 ++++ crates/aingle_graph/src/backends/mod.rs | 4 ++++ crates/aingle_graph/src/backends/rocksdb.rs | 4 ++++ crates/aingle_graph/src/backends/sled.rs | 10 ++++++++ crates/aingle_graph/src/backends/sqlite.rs | 4 ++++ crates/aingle_graph/src/dag/backend.rs | 10 ++++++++ crates/aingle_graph/src/dag/store.rs | 26 +++++++++++++++++++++ crates/aingle_graph/src/lib.rs | 24 +++++++++++++++---- crates/aingle_graph/src/store.rs | 6 +++++ 9 files changed, 88 insertions(+), 4 deletions(-) diff --git a/crates/aingle_graph/src/backends/memory.rs b/crates/aingle_graph/src/backends/memory.rs index 84c4a397..ea929319 100644 --- a/crates/aingle_graph/src/backends/memory.rs +++ b/crates/aingle_graph/src/backends/memory.rs @@ -92,6 +92,10 @@ impl StorageBackend for MemoryBackend { .map(|t| t.values().map(|v| v.len()).sum()) .unwrap_or(0) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } #[cfg(test)] diff --git a/crates/aingle_graph/src/backends/mod.rs b/crates/aingle_graph/src/backends/mod.rs index bcbecd02..9d6b8e46 100644 --- a/crates/aingle_graph/src/backends/mod.rs +++ b/crates/aingle_graph/src/backends/mod.rs @@ -65,6 +65,10 @@ pub trait StorageBackend: Send + Sync { fn close(&self) -> Result<()> { Ok(()) } + + /// Downcasting support, so callers can access backend-specific handles + /// (e.g. the shared `sled::Db` used by the persistent DAG). + fn as_any(&self) -> &dyn std::any::Any; } // Re-exports diff --git a/crates/aingle_graph/src/backends/rocksdb.rs b/crates/aingle_graph/src/backends/rocksdb.rs index 37c0a100..f71e7840 100644 --- a/crates/aingle_graph/src/backends/rocksdb.rs +++ b/crates/aingle_graph/src/backends/rocksdb.rs @@ -111,6 +111,10 @@ impl StorageBackend for RocksBackend { .map_err(|e| Error::Storage(format!("rocksdb flush error: {}", e)))?; Ok(()) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } #[cfg(test)] diff --git a/crates/aingle_graph/src/backends/sled.rs b/crates/aingle_graph/src/backends/sled.rs index 021adbbb..832bfa82 100644 --- a/crates/aingle_graph/src/backends/sled.rs +++ b/crates/aingle_graph/src/backends/sled.rs @@ -30,6 +30,12 @@ impl SledBackend { Ok(Self { db, triples }) } + /// Returns a handle to the underlying Sled database (cheaply clonable; + /// shares the same instance). Used to open the DAG tree on the same Db. + pub fn db(&self) -> &sled::Db { + &self.db + } + /// Open a temporary database (for testing) pub fn temp() -> Result { let db = sled::Config::new() @@ -115,6 +121,10 @@ impl StorageBackend for SledBackend { fn close(&self) -> Result<()> { self.flush() } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } #[cfg(test)] diff --git a/crates/aingle_graph/src/backends/sqlite.rs b/crates/aingle_graph/src/backends/sqlite.rs index 1617d47b..513dc4eb 100644 --- a/crates/aingle_graph/src/backends/sqlite.rs +++ b/crates/aingle_graph/src/backends/sqlite.rs @@ -192,6 +192,10 @@ impl StorageBackend for SqliteBackend { Ok(()) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } #[cfg(test)] diff --git a/crates/aingle_graph/src/dag/backend.rs b/crates/aingle_graph/src/dag/backend.rs index e9f15f3b..aa2e7fde 100644 --- a/crates/aingle_graph/src/dag/backend.rs +++ b/crates/aingle_graph/src/dag/backend.rs @@ -115,6 +115,16 @@ impl SledDagBackend { .map_err(|e| crate::Error::Storage(format!("sled open_tree(dag) error: {}", e)))?; Ok(Self { tree }) } + + /// Open the DAG tree on an EXISTING Sled `Db`, sharing the instance with + /// the triple store (avoids a second `sled::open` on the same path, which + /// would deadlock on the file lock). + pub fn from_db(db: &sled::Db) -> crate::Result { + let tree = db + .open_tree("dag") + .map_err(|e| crate::Error::Storage(format!("sled open_tree(dag) error: {}", e)))?; + Ok(Self { tree }) + } } #[cfg(feature = "sled-backend")] diff --git a/crates/aingle_graph/src/dag/store.rs b/crates/aingle_graph/src/dag/store.rs index b1e8a09f..d2479264 100644 --- a/crates/aingle_graph/src/dag/store.rs +++ b/crates/aingle_graph/src/dag/store.rs @@ -1769,4 +1769,30 @@ mod tests { assert_eq!(ver, vec![SCHEMA_VERSION]); } } + + /// Regression test: enabling the persistent DAG on a Sled-backed GraphDB + /// must NOT open a second `sled::Db` at the same path. Before the fix this + /// failed with "could not acquire lock on ...; another process has it + /// locked" because the triple store already held the file lock. + #[cfg(feature = "sled-backend")] + #[test] + fn enable_dag_persistent_shares_sled_db_no_lock() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("g.sled"); + let p = path.to_str().unwrap(); + + // Sled-backed GraphDB (holds the file lock on `p`). + let mut graph = crate::GraphDB::sled(p).unwrap(); + + // This must NOT error/panic (the bug was a sled lock self-conflict here). + graph + .enable_dag_persistent(p) + .expect("enable_dag_persistent must succeed on persistent sled"); + assert!(graph.dag_store().is_some()); + + // Exercise the shared Db: genesis + a DAG round-trip. + let genesis = graph.dag_store().unwrap().init_or_migrate(0).unwrap(); + let fetched = graph.dag_store().unwrap().get(&genesis).unwrap(); + assert!(fetched.is_some(), "genesis action must be readable back"); + } } diff --git a/crates/aingle_graph/src/lib.rs b/crates/aingle_graph/src/lib.rs index bf0e3863..098fca5d 100644 --- a/crates/aingle_graph/src/lib.rs +++ b/crates/aingle_graph/src/lib.rs @@ -337,8 +337,11 @@ impl GraphDB { #[cfg(all(feature = "dag", feature = "sled-backend"))] pub fn sled_with_dag(path: &str) -> Result { let backend = SledBackend::open(path)?; + // Open the DAG tree on the SAME Sled Db (shared, Arc-backed handle) to + // avoid a second `sled::open` on the same path which deadlocks on the + // file lock. + let dag_backend = dag::SledDagBackend::from_db(backend.db())?; let store = GraphStore::new(Box::new(backend))?; - let dag_backend = dag::SledDagBackend::open(path)?; Ok(Self { store, dag_store: Some(dag::DagStore::with_backend(Box::new(dag_backend))?), @@ -357,12 +360,25 @@ impl GraphDB { /// Enable DAG with a persistent Sled backend. /// - /// The DAG tree is created inside the same Sled database at `path`, - /// sharing the instance with the triple store. + /// The DAG tree is created inside the triple store's existing Sled `Db`, + /// genuinely sharing the same database instance (the handle is cloned, which + /// is cheap because `sled::Db` is `Arc`-backed). This avoids a second + /// `sled::open` on the same path, which would deadlock on the file lock. + /// If the triple store is not Sled-backed, falls back to opening `path`. #[cfg(all(feature = "dag", feature = "sled-backend"))] pub fn enable_dag_persistent(&mut self, path: &str) -> Result<()> { if self.dag_store.is_none() { - let dag_backend = dag::SledDagBackend::open(path)?; + // Share the triple store's Sled Db so we do NOT open a second + // sled::Db at the same path (which deadlocks on the file lock). + let dag_backend = if let Some(sled_be) = self + .store + .backend_as_any() + .downcast_ref::() + { + dag::SledDagBackend::from_db(sled_be.db())? + } else { + dag::SledDagBackend::open(path)? + }; self.dag_store = Some(dag::DagStore::with_backend(Box::new(dag_backend))?); } Ok(()) diff --git a/crates/aingle_graph/src/store.rs b/crates/aingle_graph/src/store.rs index e56fcbd6..83bbb7b8 100644 --- a/crates/aingle_graph/src/store.rs +++ b/crates/aingle_graph/src/store.rs @@ -249,6 +249,12 @@ impl GraphStore { self.backend.count() } + /// Access the underlying storage backend as `Any` for downcasting + /// (e.g. to reach the Sled `Db` for the shared persistent DAG). + pub fn backend_as_any(&self) -> &dyn std::any::Any { + self.backend.as_any() + } + /// Flushes any buffered writes to the underlying storage backend. /// /// For persistent backends (e.g., Sled), this ensures all data is