use std::sync::{Arc, Mutex};
use tokio::sync::Semaphore;
use dejadb_core::error::{DejaDbError, Hash, Result};
use dejadb_core::types::{Grain, GrainType};
use crate::{DejaDB, DejaDbOptions, DeserializedGrain, StoreStats};
struct Shared {
db: Mutex<Option<DejaDB>>,
gate: Semaphore,
}
#[derive(Clone)]
pub struct AsyncDejaDB {
inner: Arc<Shared>,
}
impl AsyncDejaDB {
pub async fn open(path: &str) -> Result<Self> {
let p = path.to_owned();
Self::opened(move || DejaDB::open(&p)).await
}
pub async fn open_with(path: &str, opts: DejaDbOptions) -> Result<Self> {
let p = path.to_owned();
Self::opened(move || DejaDB::open_with(&p, opts)).await
}
pub async fn open_encrypted(path: &str, key: [u8; 32]) -> Result<Self> {
let p = path.to_owned();
let key = zeroize::Zeroizing::new(key);
Self::opened(move || DejaDB::open_encrypted(&p, *key)).await
}
async fn opened<F>(open: F) -> Result<Self>
where
F: FnOnce() -> Result<DejaDB> + Send + 'static,
{
let db = offload(open).await?;
Ok(Self {
inner: Arc::new(Shared {
db: Mutex::new(Some(db)),
gate: Semaphore::new(1),
}),
})
}
pub async fn with<T, F>(&self, op: F) -> Result<T>
where
F: FnOnce(&mut DejaDB) -> Result<T> + Send + 'static,
T: Send + 'static,
{
let _permit = self.inner.gate.acquire().await.map_err(|_| closed())?;
let inner = Arc::clone(&self.inner);
offload(move || {
let mut guard = inner.db.lock().map_err(|_| poisoned())?;
let db = guard.as_mut().ok_or_else(closed)?;
op(db)
})
.await
}
pub async fn add<G>(&self, grain: G) -> Result<Hash>
where
G: Grain + Send + 'static,
{
self.with(move |db| db.add(&grain)).await
}
pub async fn add_if_novel<G>(&self, grain: G) -> Result<(Hash, bool)>
where
G: Grain + Send + 'static,
{
self.with(move |db| db.add_if_novel(&grain)).await
}
pub async fn latest(
&self,
ns: &str,
subject: &str,
relation: &str,
) -> Result<Option<DeserializedGrain>> {
let (ns, subject, relation) = (ns.to_owned(), subject.to_owned(), relation.to_owned());
self.with(move |db| db.latest(&ns, &subject, &relation))
.await
}
pub async fn recall_hybrid(
&self,
ns: &str,
subject: Option<&str>,
relation: Option<&str>,
query: Option<&str>,
k: usize,
deadline: Option<std::time::Duration>,
) -> Result<Vec<DeserializedGrain>> {
let ns = ns.to_owned();
let subject = subject.map(str::to_owned);
let relation = relation.map(str::to_owned);
let query = query.map(str::to_owned);
self.with(move |db| {
db.recall_hybrid(
&ns,
subject.as_deref(),
relation.as_deref(),
query.as_deref(),
k,
deadline,
)
})
.await
}
pub async fn recent(
&self,
ns: &str,
gtype: Option<GrainType>,
limit: usize,
) -> Result<Vec<DeserializedGrain>> {
let ns = ns.to_owned();
self.with(move |db| db.recent(&ns, gtype, limit)).await
}
pub async fn forget(&self, hash: Hash) -> Result<()> {
self.with(move |db| db.forget(&hash)).await
}
pub async fn stats(&self) -> Result<StoreStats> {
self.with(|db| db.stats()).await
}
pub async fn close(self) -> Result<()> {
let _permit = self.inner.gate.acquire().await.map_err(|_| closed())?;
let taken = {
let mut guard = self.inner.db.lock().map_err(|_| poisoned())?;
guard.take()
};
match taken {
Some(db) => {
offload(move || {
drop(db);
Ok(())
})
.await
}
None => Ok(()),
}
}
}
impl Drop for AsyncDejaDB {
fn drop(&mut self) {
if let Some(db) = Arc::get_mut(&mut self.inner)
.and_then(|s| s.db.get_mut().ok())
.and_then(Option::take)
{
std::thread::spawn(move || drop(db));
}
}
}
async fn offload<T, F>(op: F) -> Result<T>
where
F: FnOnce() -> Result<T> + Send + 'static,
T: Send + 'static,
{
match tokio::task::spawn_blocking(op).await {
Ok(r) => r,
Err(e) => Err(DejaDbError::Storage(format!(
"dejadb blocking task failed: {e}"
))),
}
}
fn poisoned() -> DejaDbError {
DejaDbError::Storage("dejadb handle poisoned by a panic in another task".into())
}
fn closed() -> DejaDbError {
DejaDbError::Storage("dejadb handle already closed".into())
}
#[cfg(test)]
mod tests {
use super::*;
use dejadb_core::types::Fact;
fn tmp(name: &str) -> String {
let dir = tempfile::tempdir().expect("tempdir").keep();
dir.join(name).to_string_lossy().into_owned()
}
#[tokio::test]
async fn is_usable_from_async_code() {
let path = tmp("async-usable.db");
let db = AsyncDejaDB::open(&path).await.expect("open");
let mut fact = Fact::new("john", "prefers", "dark mode");
fact.common_mut().namespace = Some("caller".to_string());
db.add(fact).await.expect("add");
let got = db
.latest("caller", "john", "prefers")
.await
.expect("latest")
.expect("a grain");
assert_eq!(
got.fields.get("object").and_then(|v| v.as_str()),
Some("dark mode"),
);
drop(db);
}
#[tokio::test]
async fn escape_hatch_runs_arbitrary_ops_safely() {
let path = tmp("async-with.db");
let db = AsyncDejaDB::open(&path).await.expect("open");
let stats = db.with(|db| db.stats()).await.expect("stats via with");
assert_eq!(stats.grains, 0);
}
#[tokio::test]
async fn close_awaits_teardown() {
let path = tmp("async-close.db");
let db = AsyncDejaDB::open(&path).await.expect("open");
let mut fact = Fact::new("grace", "built", "the compiler");
fact.common_mut().namespace = Some("caller".to_string());
db.add(fact).await.expect("add");
db.close().await.expect("close");
let again = AsyncDejaDB::open(&path).await.expect("reopen");
assert!(again
.latest("caller", "grace", "built")
.await
.expect("latest")
.is_some());
again.close().await.expect("close again");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_operations_queue_without_hogging_the_blocking_pool() {
let path = tmp("async-concurrent.db");
let db = AsyncDejaDB::open(&path).await.expect("open");
let writes = (0..64).map(|i| {
let db = db.clone();
tokio::spawn(async move {
let mut fact = Fact::new(&format!("s{i}"), "n", &i.to_string());
fact.common_mut().namespace = Some("caller".to_string());
db.add(fact).await
})
});
for w in writes {
w.await.expect("task").expect("add");
}
let stats = db.stats().await.expect("stats");
assert_eq!(stats.grains, 64);
db.close().await.expect("close");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn clones_share_one_store() {
let path = tmp("async-clone.db");
let db = AsyncDejaDB::open(&path).await.expect("open");
let handle = db.clone();
let mut fact = Fact::new("linus", "wrote", "git");
fact.common_mut().namespace = Some("caller".to_string());
handle.add(fact).await.expect("add via clone");
drop(handle);
assert!(db
.latest("caller", "linus", "wrote")
.await
.expect("latest via original")
.is_some());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn works_on_a_multi_thread_runtime() {
let path = tmp("async-multi.db");
let db = AsyncDejaDB::open(&path).await.expect("open");
let mut fact = Fact::new("ada", "wrote", "the first program");
fact.common_mut().namespace = Some("caller".to_string());
db.add(fact).await.expect("add");
assert!(db
.latest("caller", "ada", "wrote")
.await
.expect("latest")
.is_some());
}
}