use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::commit::validation::Mutation;
use crate::{AedbConfig, AedbInstance, CompleteEffect, ConsistencyMode, EffectClaim, EffectStatus};
use tempfile::tempdir;
async fn open() -> (tempfile::TempDir, AedbInstance) {
let dir = tempdir().expect("temp");
let db = AedbInstance::open_anonymous(AedbConfig::default(), dir.path()).expect("open");
db.create_project("arcana").await.expect("project");
db.create_scope("arcana", "app").await.expect("scope");
(dir, db)
}
fn credit(key: &str) -> Vec<Mutation> {
vec![Mutation::KvSet {
project_id: "arcana".into(),
scope_id: "app".into(),
key: key.as_bytes().to_vec(),
value: b"credited".to_vec(),
}]
}
#[tokio::test]
async fn begin_then_complete_then_duplicate() {
let (_dir, db) = open().await;
let c1 = db.begin_effect("arcana", "app", "dep-1").await.unwrap();
assert_eq!(c1, EffectClaim::Fresh);
let c2 = db.begin_effect("arcana", "app", "dep-1").await.unwrap();
assert_eq!(c2, EffectClaim::InProgress { attempts: 2 });
let done = db
.complete_effect(
"arcana",
"app",
"dep-1",
r#"{"tx":"0xabc"}"#.into(),
credit("c1"),
)
.await
.unwrap();
assert!(matches!(done, CompleteEffect::Applied(_)));
let v = db
.kv_get_no_auth("arcana", "app", b"c1", ConsistencyMode::AtLatest)
.await
.unwrap();
assert_eq!(v.map(|e| e.value), Some(b"credited".to_vec()));
let c3 = db.begin_effect("arcana", "app", "dep-1").await.unwrap();
assert_eq!(
c3,
EffectClaim::AlreadyCommitted {
result_json: r#"{"tx":"0xabc"}"#.into()
}
);
}
#[tokio::test]
async fn complete_is_exactly_once() {
let (_dir, db) = open().await;
let first = db
.complete_effect("arcana", "app", "dep-2", "{}".into(), credit("once"))
.await
.unwrap();
assert!(matches!(first, CompleteEffect::Applied(_)));
let second = db
.complete_effect(
"arcana",
"app",
"dep-2",
"{}".into(),
vec![Mutation::KvSet {
project_id: "arcana".into(),
scope_id: "app".into(),
key: b"once".to_vec(),
value: b"DOUBLED".to_vec(),
}],
)
.await
.unwrap();
assert!(matches!(second, CompleteEffect::AlreadyCommitted { .. }));
let v = db
.kv_get_no_auth("arcana", "app", b"once", ConsistencyMode::AtLatest)
.await
.unwrap();
assert_eq!(v.map(|e| e.value), Some(b"credited".to_vec()));
}
#[tokio::test]
async fn concurrent_complete_applies_exactly_one() {
let (_dir, db) = open().await;
let db = Arc::new(db);
let applied = Arc::new(AtomicUsize::new(0));
let already = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for worker in 0..12u8 {
let db = Arc::clone(&db);
let applied = Arc::clone(&applied);
let already = Arc::clone(&already);
handles.push(tokio::spawn(async move {
let muts = vec![Mutation::KvSet {
project_id: "arcana".into(),
scope_id: "app".into(),
key: b"winner".to_vec(),
value: vec![worker],
}];
match db
.complete_effect("arcana", "app", "race", "{}".into(), muts)
.await
.unwrap()
{
CompleteEffect::Applied(_) => applied.fetch_add(1, Ordering::SeqCst),
CompleteEffect::AlreadyCommitted { .. } => already.fetch_add(1, Ordering::SeqCst),
};
}));
}
for h in handles {
h.await.unwrap();
}
assert_eq!(applied.load(Ordering::SeqCst), 1, "exactly one applied");
assert_eq!(already.load(Ordering::SeqCst), 11, "rest are duplicates");
let v = db
.kv_get_no_auth("arcana", "app", b"winner", ConsistencyMode::AtLatest)
.await
.unwrap();
assert!(v.is_some());
}
#[tokio::test]
async fn status_reflects_lifecycle() {
let (_dir, db) = open().await;
assert!(
db.effect_status("arcana", "app", "dep-3", ConsistencyMode::AtLatest)
.await
.unwrap()
.is_none()
);
db.begin_effect("arcana", "app", "dep-3").await.unwrap();
let pending = db
.effect_status("arcana", "app", "dep-3", ConsistencyMode::AtLatest)
.await
.unwrap()
.unwrap();
assert_eq!(pending.status, EffectStatus::Pending);
assert_eq!(pending.attempts, 1);
db.complete_effect("arcana", "app", "dep-3", r#"{"ok":true}"#.into(), vec![])
.await
.unwrap();
let committed = db
.effect_status("arcana", "app", "dep-3", ConsistencyMode::AtLatest)
.await
.unwrap()
.unwrap();
assert_eq!(committed.status, EffectStatus::Committed);
assert_eq!(committed.result_json, r#"{"ok":true}"#);
}
#[tokio::test]
async fn rejects_empty_key_and_bad_json() {
let (_dir, db) = open().await;
assert!(db.begin_effect("arcana", "app", "").await.is_err());
assert!(
db.complete_effect("arcana", "app", "k", "not json".into(), vec![])
.await
.is_err()
);
}
#[tokio::test]
async fn survives_restart() {
let dir = tempdir().expect("temp");
{
let db = AedbInstance::open_anonymous(AedbConfig::default(), dir.path()).expect("open");
db.create_project("arcana").await.expect("project");
db.create_scope("arcana", "app").await.expect("scope");
db.complete_effect("arcana", "app", "persist", r#"{"x":1}"#.into(), vec![])
.await
.unwrap();
db.shutdown().await.expect("graceful shutdown");
}
let db2 = AedbInstance::open_anonymous(AedbConfig::default(), dir.path()).expect("reopen");
let claim = db2.begin_effect("arcana", "app", "persist").await.unwrap();
assert_eq!(
claim,
EffectClaim::AlreadyCommitted {
result_json: r#"{"x":1}"#.into()
}
);
}