use super::*;
use rushdb_types::command::Mutation;
fn summing_expander() -> Arc<ExpandFn> {
Arc::new(Box::new(|_ci, batch, txn| {
for mutation in &batch.mutations {
let Mutation::PutRawKey { key, value } = mutation else {
continue;
};
let delta = i64::from_be_bytes(
value
.as_slice()
.try_into()
.map_err(|_| "delta must be 8 bytes".to_string())?,
);
let current = txn
.get(key)
.map_err(|error| error.to_string())?
.map_or(0, |raw| {
raw.as_slice().try_into().map_or(0, i64::from_be_bytes)
});
txn.put(key.clone(), (current + delta).to_be_bytes().to_vec());
}
Ok(())
}))
}
fn delta_batch(commit_key: Uuid, tenant: TenantId, deltas: &[(Vec<u8>, i64)]) -> CommitBatch {
CommitBatch {
command_version: rushdb_types::command::COMMAND_VERSION,
tenant,
transaction_id: format!("seq-{commit_key}"),
commit_key,
start_index: 0,
isolation: rushdb_types::command::IsolationMode::Snapshot,
preconditions: vec![],
mutations: deltas
.iter()
.map(|(key, delta)| Mutation::PutRawKey {
key: key.clone(),
value: delta.to_be_bytes().to_vec(),
})
.collect(),
}
}
fn xorshift(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
fn workload() -> Vec<Vec<(Vec<u8>, i64)>> {
let mut steps: Vec<Vec<(Vec<u8>, i64)>> = vec![
vec![(b"acct/a".to_vec(), 5)],
vec![(b"acct/a".to_vec(), -2), (b"acct/b".to_vec(), 7)],
vec![(b"acct/b".to_vec(), 3), (b"acct/b".to_vec(), 1)],
vec![(b"acct/c".to_vec(), 9)],
];
let accounts = [b"acct/a".to_vec(), b"acct/b".to_vec(), b"acct/c".to_vec()];
let mut state = 0x9E37_79B9_7F4A_7C15_u64;
for _ in 0..28 {
let count = 1 + usize::try_from(xorshift(&mut state) % 3).unwrap_or(0);
let mut step = Vec::with_capacity(count);
for _ in 0..count {
let index = usize::try_from(xorshift(&mut state) % 3).unwrap_or(0);
let delta = i64::try_from(xorshift(&mut state) % 7).unwrap_or(0) - 3;
step.push((accounts[index].clone(), delta));
}
steps.push(step);
}
steps
}
fn op_key(op: &Op) -> &[u8] {
match op {
Op::Put(key, _) | Op::Set(key, _) | Op::Del(key) | Op::Purge(key) => key,
}
}
fn apply_net(pending: &mut BTreeMap<Vec<u8>, Option<Vec<u8>>>, ops: &[Op]) {
for op in ops {
if is_coordination_write(op_key(op)) {
continue;
}
match op {
Op::Put(key, value) | Op::Set(key, value) => {
pending.insert(key.clone(), Some(value.clone()));
}
Op::Del(key) | Op::Purge(key) => {
pending.insert(key.clone(), None);
}
}
}
}
#[test]
fn sequencer_buffer_trims_published_and_tracks_in_flight() {
let store = Store::new_in_memory();
store.set_expander(summing_expander());
let tenant = TenantId::from_ordinal(1);
let first = Uuid::new_v4();
let batch = delta_batch(first, tenant, &[(b"acct".to_vec(), 5)]);
store
.commit(first, |_, _| Ok((batch.clone(), ())))
.expect("first commit");
{
let state = store.admission_lock.lock().unwrap();
assert_eq!(
state.buffer.get(b"acct".as_slice()).map(|(ci, _)| *ci),
Some(1)
);
}
let second = Uuid::new_v4();
let batch = delta_batch(second, tenant, &[(b"acct".to_vec(), 3)]);
store
.commit(second, |_, _| Ok((batch.clone(), ())))
.expect("second commit");
{
let state = store.admission_lock.lock().unwrap();
assert_eq!(
state.buffer.get(b"acct".as_slice()).map(|(ci, _)| *ci),
Some(2),
"the published entry is trimmed and the new in-flight one recorded"
);
}
}
#[test]
fn concurrent_commits_coalesce_into_group_commits() {
let config = resources::StorageResources {
max_group_delay_ms: 20,
..Default::default()
};
let store = Arc::new(Store::new_with_resources(config).unwrap());
store.set_expander(Arc::new(Box::new(|_ci, batch, txn| {
let mut key = b"key/".to_vec();
key.extend_from_slice(batch.commit_key.as_bytes());
txn.put(key, vec![1]);
Ok(())
})));
let tenant = TenantId::from_ordinal(1);
let threads = 8usize;
let per_thread = 50usize;
let barrier = Arc::new(std::sync::Barrier::new(threads));
let mut handles = Vec::new();
for _ in 0..threads {
let store = Arc::clone(&store);
let barrier = Arc::clone(&barrier);
handles.push(std::thread::spawn(move || {
barrier.wait();
for _ in 0..per_thread {
let commit_key = Uuid::new_v4();
let batch = delta_batch(commit_key, tenant, &[]);
store
.commit(commit_key, |_, _| Ok((batch.clone(), ())))
.expect("concurrent commit");
}
}));
}
for handle in handles {
handle.join().unwrap();
}
let total = (threads * per_thread) as u64;
assert_eq!(store.committed_version(), total);
let groups = store.group_commits.load(Ordering::Relaxed);
assert!(
groups < total,
"expected coalescing: {groups} group-commits for {total} commits"
);
}
#[test]
fn dependent_sequenced_commits_share_publish_groups_and_stay_exact() {
let config = resources::StorageResources {
max_group_delay_ms: 20,
..Default::default()
};
let store = Arc::new(Store::new_with_resources(config).unwrap());
store.set_expander(summing_expander());
let tenant = TenantId::from_ordinal(1);
let threads = 8usize;
let per_thread = 50usize;
let barrier = Arc::new(std::sync::Barrier::new(threads));
let handles: Vec<_> = (0..threads)
.map(|_| {
let (store, barrier) = (Arc::clone(&store), Arc::clone(&barrier));
std::thread::spawn(move || {
barrier.wait();
for _ in 0..per_thread {
let commit_key = Uuid::new_v4();
let batch = delta_batch(commit_key, tenant, &[(b"acct/hot".to_vec(), 1)]);
store
.commit(commit_key, |_, _| Ok((batch.clone(), ())))
.expect("dependent commit");
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
let total = (threads * per_thread) as u64;
assert_eq!(store.committed_version(), total);
let hot = store.try_get(b"acct/hot").unwrap().unwrap();
assert_eq!(
i64::from_be_bytes(hot.as_slice().try_into().unwrap()),
i64::try_from(total).unwrap(),
"every dependent increment must land exactly once"
);
let groups = store.group_commits.load(Ordering::Relaxed);
assert!(
groups * 2 < total,
"dependent commits must coalesce: {groups} groups for {total} commits"
);
}
#[test]
fn overlay_expansion_matches_the_committed_forward_path() {
let store = Store::new_in_memory();
store.set_expander(summing_expander());
store.set_receipt_clock(Arc::new(|| 1_000));
let tenant = TenantId::from_ordinal(7);
let workload = workload();
let accounts: [&[u8]; 3] = [b"acct/a", b"acct/b", b"acct/c"];
let mut committed: Vec<(u64, CommitBatch, String)> = Vec::new();
for deltas in &workload {
let commit_key = Uuid::new_v4();
let batch = delta_batch(commit_key, tenant, deltas);
let payload = format!("payload-{}", committed.len());
let outcome = store
.commit(commit_key, |_, _| Ok((batch.clone(), payload.clone())))
.expect("commit");
let ci = match outcome {
CommitOutcome::Committed { commit_index, .. } => commit_index,
CommitOutcome::Conflicted(reason) => panic!("unexpected conflict: {reason}"),
};
committed.push((ci, batch, payload));
}
assert_eq!(committed.len(), workload.len());
let base_ci = committed.first().map_or(0, |(ci, ..)| ci - 1);
let mut pending: BTreeMap<Vec<u8>, Option<Vec<u8>>> = BTreeMap::new();
for (step, (ci, batch, payload)) in committed.iter().enumerate() {
let overlay = Arc::new(OverlayView::with_buffer(
Arc::new(store.read_at(base_ci).expect("base view")),
pending.clone(),
));
let overlay_ops = store
.expand_against_overlay(*ci, batch, payload, Arc::clone(&overlay))
.expect("overlay expansion");
let predecessor = Arc::new(OverlayView::new(Arc::new(
store.read_at(ci - 1).expect("predecessor view"),
)));
let forward_ops = store
.expand_against_overlay(*ci, batch, payload, predecessor)
.expect("forward expansion");
assert_eq!(
overlay_ops, forward_ops,
"overlay expansion diverged from the committed forward path at step {step} (ci {ci})"
);
apply_net(&mut pending, &overlay_ops);
let predicted = OverlayView::with_buffer(
Arc::new(store.read_at(base_ci).expect("base view")),
pending.clone(),
);
let real = store.read_at(*ci).expect("real view");
for account in accounts {
assert_eq!(
predicted.get(account).unwrap(),
real.get(account).unwrap(),
"overlay state diverged from committed state for {:?} at ci {ci}",
String::from_utf8_lossy(account)
);
}
}
}