Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/aingle_graph/src/backends/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
4 changes: 4 additions & 0 deletions crates/aingle_graph/src/backends/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions crates/aingle_graph/src/backends/rocksdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
10 changes: 10 additions & 0 deletions crates/aingle_graph/src/backends/sled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
let db = sled::Config::new()
Expand Down Expand Up @@ -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)]
Expand Down
4 changes: 4 additions & 0 deletions crates/aingle_graph/src/backends/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ impl StorageBackend for SqliteBackend {

Ok(())
}

fn as_any(&self) -> &dyn std::any::Any {
self
}
}

#[cfg(test)]
Expand Down
10 changes: 10 additions & 0 deletions crates/aingle_graph/src/dag/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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")]
Expand Down
26 changes: 26 additions & 0 deletions crates/aingle_graph/src/dag/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
24 changes: 20 additions & 4 deletions crates/aingle_graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,11 @@ impl GraphDB {
#[cfg(all(feature = "dag", feature = "sled-backend"))]
pub fn sled_with_dag(path: &str) -> Result<Self> {
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))?),
Expand All @@ -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::<crate::backends::SledBackend>()
{
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(())
Expand Down
6 changes: 6 additions & 0 deletions crates/aingle_graph/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading