use std::collections::BTreeSet;
use std::sync::{Arc, Mutex};
use nodedb::bridge::dispatch::Dispatcher;
use nodedb::control::cluster::calvin::scheduler::lock_manager::{
AcquireOutcome, LockKey, LockManager, TxnId,
};
use nodedb::control::server::shared::write_admission::{
WriteAdmission, WriteTarget, admit, cp_routed_to_calvin,
};
use nodedb::control::state::SharedState;
use nodedb::types::{DatabaseId, TenantId, VShardId};
use nodedb::wal::WalManager;
use nodedb_physical::physical_plan::{KvOp, PhysicalPlan};
use nodedb_types::Surrogate;
fn build_shared() -> (Arc<SharedState>, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("tempdir");
let wal =
Arc::new(WalManager::open_for_testing(&dir.path().join("fence.wal")).expect("open wal"));
let (dispatcher, _data_sides) = Dispatcher::new(1, 64);
let shared = SharedState::new(dispatcher, wal).expect("shared state");
(shared, dir)
}
fn register_lock_manager(
shared: &SharedState,
collection: &str,
) -> (Arc<Mutex<LockManager>>, VShardId) {
let vshard = VShardId::from_collection_in_database(DatabaseId::DEFAULT, collection);
let lm = Arc::new(Mutex::new(LockManager::new()));
shared
.calvin_lock_managers
.lock()
.expect("lock managers")
.insert(vshard.as_u32(), Arc::clone(&lm));
(lm, vshard)
}
fn register_promotion_channel(
shared: &SharedState,
vshard: VShardId,
) -> tokio::sync::mpsc::UnboundedReceiver<Vec<TxnId>> {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
shared
.calvin_promotion_senders
.lock()
.expect("promotion senders")
.insert(vshard.as_u32(), tx);
rx
}
fn kv_put(collection: &str, key: &[u8]) -> PhysicalPlan {
PhysicalPlan::Kv(KvOp::Put {
collection: collection.to_owned(),
key: key.to_vec(),
value: b"v".to_vec(),
ttl_ms: 0,
surrogate: Surrogate::ZERO,
})
}
fn kv_lock_key(collection: &str, key: &[u8]) -> LockKey {
LockKey::Kv {
collection: Arc::from(collection),
key: Arc::from(key),
}
}
fn target<'a>(vshard: VShardId, plan: &'a PhysicalPlan) -> WriteTarget<'a> {
WriteTarget {
tenant_id: TenantId::new(1),
database_id: DatabaseId::DEFAULT,
vshard_id: vshard,
plan,
}
}
#[tokio::test]
async fn fence_write_blocks_behind_held_commit_lock() {
let (shared, _dir) = build_shared();
let coll = "fence_coll";
let (lm, vshard) = register_lock_manager(&shared, coll);
let commit_txn = TxnId::new(5, 0);
let held: BTreeSet<LockKey> = [kv_lock_key(coll, b"K")].into();
assert_eq!(
lm.lock().expect("lm").acquire(commit_txn, held),
AcquireOutcome::Ready,
"the simulated commit takes the lock first"
);
let plan = kv_put(coll, b"K");
let before = cp_routed_to_calvin();
match admit(&shared, &target(vshard, &plan)) {
WriteAdmission::RouteToCalvin => {}
_ => panic!("a point write behind a held commit lock must route to Calvin"),
}
assert!(
cp_routed_to_calvin() > before,
"the routed write must bump the routed-to-Calvin counter"
);
let _ = lm.lock().expect("lm").release(commit_txn);
match admit(&shared, &target(vshard, &plan)) {
WriteAdmission::FastPath { guard: Some(_) } => {}
_ => panic!("after release, the point write must fast-path with a real lock guard"),
}
}
#[tokio::test]
async fn two_concurrent_same_key_point_writes_serialize() {
let (shared, _dir) = build_shared();
let coll = "serialize_coll";
let (_lm, vshard) = register_lock_manager(&shared, coll);
let plan = kv_put(coll, b"K");
let guard1 = match admit(&shared, &target(vshard, &plan)) {
WriteAdmission::FastPath { guard: Some(g) } => g,
_ => panic!("first same-key write must fast-path with a real lock guard"),
};
let before = cp_routed_to_calvin();
match admit(&shared, &target(vshard, &plan)) {
WriteAdmission::RouteToCalvin => {}
_ => panic!("second same-key write must route to Calvin behind the first"),
}
assert!(
cp_routed_to_calvin() > before,
"the second write must bump the routed-to-Calvin counter"
);
drop(guard1);
match admit(&shared, &target(vshard, &plan)) {
WriteAdmission::FastPath { guard: Some(_) } => {}
_ => panic!("after the first write releases, the key must fast-path again"),
}
}
#[tokio::test]
async fn single_node_point_write_uses_global_keyed_order_lock() {
let (shared, _dir) = build_shared();
let coll = "single_node_coll";
let vshard = VShardId::from_collection_in_database(DatabaseId::DEFAULT, coll);
let plan = kv_put(coll, b"K");
let lock = match admit(&shared, &target(vshard, &plan)) {
WriteAdmission::FastPathBlocking { key, keyed_lock } => {
assert_eq!(
key,
kv_lock_key(coll, b"K"),
"the admission must carry the write's exact point key"
);
keyed_lock
}
_ => panic!("a single-node point write must return FastPathBlocking"),
};
assert!(
Arc::ptr_eq(&lock, &shared.write_order_locks),
"the gate must hand out the one global SharedState keyed order-lock"
);
}
#[tokio::test]
async fn single_node_same_key_serializes_fifo() {
let (shared, _dir) = build_shared();
let coll = "single_node_fifo";
let vshard = VShardId::from_collection_in_database(DatabaseId::DEFAULT, coll);
let plan = kv_put(coll, b"K");
let (key, lock) = match admit(&shared, &target(vshard, &plan)) {
WriteAdmission::FastPathBlocking { key, keyed_lock } => (key, keyed_lock),
_ => panic!("single-node point write must return FastPathBlocking"),
};
let order = Arc::new(Mutex::new(Vec::<u32>::new()));
let held = lock.lock_owned(key.clone()).await;
let mut handles = Vec::new();
for id in [1u32, 2u32] {
let lock = Arc::clone(&lock);
let order = Arc::clone(&order);
let key = key.clone();
handles.push(tokio::spawn(async move {
let _g = lock.lock_owned(key).await;
order.lock().expect("order").push(id);
}));
tokio::task::yield_now().await;
tokio::task::yield_now().await;
}
assert!(
order.lock().expect("order").is_empty(),
"same-key waiters must block while the holder is live"
);
drop(held);
for h in handles {
h.await.expect("waiter task");
}
assert_eq!(
*order.lock().expect("order"),
vec![1, 2],
"concurrent same-key writes must acquire in FIFO arrival order"
);
}
#[tokio::test]
async fn fast_path_drop_delivers_promoted_scheduler_txn() {
let (shared, _dir) = build_shared();
let coll = "promotion_coll";
let (lm, vshard) = register_lock_manager(&shared, coll);
let mut promotion_rx = register_promotion_channel(&shared, vshard);
let plan = kv_put(coll, b"K");
let guard = match admit(&shared, &target(vshard, &plan)) {
WriteAdmission::FastPath { guard: Some(g) } => g,
_ => panic!("uncontended point write must fast-path with a real lock guard"),
};
let scheduler_txn = TxnId::new(7, 0);
let want: BTreeSet<LockKey> = [kv_lock_key(coll, b"K")].into();
assert_eq!(
lm.lock().expect("lm").acquire(scheduler_txn, want.clone()),
AcquireOutcome::Blocked,
"the scheduler txn must block behind the fast-path holder"
);
assert!(
promotion_rx.try_recv().is_err(),
"no promotion may be delivered while the fast-path holder is live"
);
drop(guard);
let promoted = promotion_rx
.try_recv()
.expect("the promoted scheduler txn must be delivered over the promotion channel");
assert!(
promoted.contains(&scheduler_txn),
"the delivered promotion set must name the unblocked scheduler txn"
);
assert!(
lm.lock().expect("lm").is_ready(scheduler_txn, &want),
"release must have installed the scheduler txn as holder of the freed key"
);
}
#[tokio::test]
async fn single_node_distinct_keys_do_not_block() {
let (shared, _dir) = build_shared();
let coll = "single_node_distinct";
let lock = Arc::clone(&shared.write_order_locks);
let g_a = lock.lock_owned(kv_lock_key(coll, b"A")).await;
let g_b = tokio::time::timeout(
std::time::Duration::from_secs(5),
lock.lock_owned(kv_lock_key(coll, b"B")),
)
.await
.expect("a distinct key must not block on a held key");
drop(g_b);
drop(g_a);
}