use aedb::AedbInstance;
use aedb::config::AedbConfig;
use aedb::error::AedbError;
use aedb::locks::LockKey;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tempfile::tempdir;
async fn open(dir: &std::path::Path) -> Arc<AedbInstance> {
let db = AedbInstance::open_anonymous(Default::default(), dir).expect("open");
Arc::new(db)
}
async fn reopen(config: AedbConfig, dir: &std::path::Path) -> Arc<AedbInstance> {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match AedbInstance::open_anonymous(config.clone(), dir) {
Ok(db) => return Arc::new(db),
Err(AedbError::Unavailable { .. }) if Instant::now() < deadline => {
tokio::time::sleep(Duration::from_millis(20)).await;
}
Err(err) => panic!("reopen: {err:?}"),
}
}
}
#[tokio::test]
async fn lock_serializes_modifications_while_reads_stay_concurrent() {
let dir = tempdir().expect("temp");
let db = open(dir.path()).await;
db.create_project("p").await.expect("project");
db.kv_set("p", "app", b"program:1/state".to_vec(), b"v0".to_vec())
.await
.expect("seed");
let key = LockKey::new("p", "app", b"program:1".to_vec());
let mut tx_a = db.lock_scope();
tx_a.lock(key.clone()).expect("A acquires lock");
let mut tx_b = db.lock_scope();
assert_eq!(
tx_b.try_lock(key.clone()).expect("try_lock ok"),
None,
"second execution is locked out of the same program"
);
db.snapshot_probe(aedb::query::plan::ConsistencyMode::AtLatest)
.await
.expect("snapshot read is not blocked by the object lock");
db.kv_set("p", "app", b"program:1/state".to_vec(), b"v1".to_vec())
.await
.expect("guarded write");
tx_a.commit();
assert!(
tx_b.try_lock(key.clone()).expect("try_lock ok").is_some(),
"lock is available once A commits"
);
}
#[tokio::test]
async fn lock_released_on_rollback() {
let dir = tempdir().expect("temp");
let db = open(dir.path()).await;
let key = LockKey::global("workflow:7");
let mut tx = db.lock_scope();
tx.lock(key.clone()).expect("acquire");
assert!(db.lock_manager().is_locked(&key));
tx.rollback();
assert!(
!db.lock_manager().is_locked(&key),
"rollback releases the lock"
);
}
#[tokio::test]
async fn lock_released_when_scope_dropped() {
let dir = tempdir().expect("temp");
let db = open(dir.path()).await;
let key = LockKey::global("doc:99");
{
let mut tx = db.lock_scope();
tx.lock(key.clone()).expect("acquire");
}
assert!(
!db.lock_manager().is_locked(&key),
"dropping the scope releases the lock"
);
}
#[tokio::test]
async fn blocking_acquire_times_out_under_contention() {
let dir = tempdir().expect("temp");
let db = open(dir.path()).await;
let key = LockKey::global("k");
let mut holder = db.lock_scope();
holder.lock(key.clone()).expect("acquire");
let mut waiter = db.lock_scope();
let err = waiter
.lock_with_timeout(key.clone(), Duration::from_millis(50))
.unwrap_err();
assert!(
matches!(err, AedbError::Timeout),
"blocking acquire degrades to a timeout rather than hanging"
);
}
#[tokio::test]
async fn lock_all_acquires_multiple_objects_atomically() {
let dir = tempdir().expect("temp");
let db = open(dir.path()).await;
let mut tx = db.lock_scope();
tx.lock_all(vec![
LockKey::global("a"),
LockKey::global("b"),
LockKey::global("c"),
])
.expect("acquire all");
assert_eq!(db.lock_manager().held_count(), 3);
tx.commit();
assert_eq!(db.lock_manager().held_count(), 0);
}
#[tokio::test]
async fn locks_do_not_survive_restart() {
let dir = tempdir().expect("temp");
let config = AedbConfig::default();
let key = LockKey::global("program:1");
{
let db = open(dir.path()).await;
db.create_project("p").await.expect("project");
let mut tx = db.lock_scope();
tx.lock(key.clone()).expect("acquire");
assert!(db.lock_manager().is_locked(&key));
std::mem::forget(tx);
assert!(db.lock_manager().is_locked(&key));
db.shutdown().await.expect("shutdown");
}
let db2 = reopen(config, dir.path()).await;
assert!(
!db2.lock_manager().is_locked(&key),
"in-memory locks are discarded on restart"
);
assert_eq!(db2.lock_manager().held_count(), 0);
let mut tx = db2.lock_scope();
assert!(tx.try_lock(key).expect("try_lock ok").is_some());
}