use super::tests::{CountingHook, open_fixed_tree};
use crate::fixed::slot::{NO_SLOT, STATUS_OCCUPIED, pack_meta, version_of};
use crate::txlog::record::{OpKind, VersionTag};
use crate::txn::participant::{PlannedOp, TxParticipant};
#[test]
fn tx_fixed_replay_skips_an_operation_that_already_landed() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let sid = tree.shard_of(&[1u8; 8]).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &[1u8; 8],
delete: false,
}])
.unwrap();
locked.apply_put(&[1u8; 8], &[5u8; 16], tags[0]).unwrap();
drop(
locked
.finish(
&mut Vec::new(),
&mut crate::watch::WatchWakeBatch::default(),
)
.unwrap(),
);
let mut again = tree.lock_shards(&[sid]).unwrap();
assert!(
!again.needs_replay(&[1u8; 8], OpKind::Put, tags[0]).unwrap(),
"the slot already holds the committed state"
);
again.abort();
}
#[test]
fn tx_fixed_replay_replays_an_operation_that_never_landed() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let sid = tree.shard_of(&[2u8; 8]).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &[2u8; 8],
delete: false,
}])
.unwrap();
assert!(
locked
.needs_replay(&[2u8; 8], OpKind::Put, tags[0])
.unwrap()
);
locked.abort();
}
#[test]
fn tx_fixed_replay_replays_a_torn_slot_at_the_target_version() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let sid = tree.shard_of(&[3u8; 8]).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &[3u8; 8],
delete: false,
}])
.unwrap();
locked.apply_put(&[3u8; 8], &[6u8; 16], tags[0]).unwrap();
drop(
locked
.finish(
&mut Vec::new(),
&mut crate::watch::WatchWakeBatch::default(),
)
.unwrap(),
);
let VersionTag::Fixed {
slot_id,
target_meta,
} = tags[0]
else {
unreachable!("Fixed participant")
};
tree.tx_debug_force_slot_meta(
sid,
slot_id,
pack_meta(crate::fixed::slot::STATUS_FREE, version_of(target_meta)),
);
let mut again = tree.lock_shards(&[sid]).unwrap();
assert!(
again.needs_replay(&[3u8; 8], OpKind::Put, tags[0]).unwrap(),
"a torn slot at the target version must be replayed"
);
again.abort();
}
#[test]
fn tx_fixed_replay_skips_a_slot_reused_by_another_key() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let sid = tree.shard_of(&[4u8; 8]).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &[4u8; 8],
delete: false,
}])
.unwrap();
locked.apply_put(&[4u8; 8], &[1u8; 16], tags[0]).unwrap();
drop(
locked
.finish(
&mut Vec::new(),
&mut crate::watch::WatchWakeBatch::default(),
)
.unwrap(),
);
tree.delete(&[4u8; 8]).unwrap();
tree.put(&[5u8; 8], &[2u8; 16]).unwrap();
let mut again = tree.lock_shards(&[sid]).unwrap();
assert!(
!again.needs_replay(&[4u8; 8], OpKind::Put, tags[0]).unwrap(),
"the slot has moved on; replaying would clobber another key"
);
again.abort();
}
#[test]
fn tx_fixed_replay_skips_a_delete_that_already_landed() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
tree.put(&[6u8; 8], &[1u8; 16]).unwrap();
let sid = tree.shard_of(&[6u8; 8]).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &[6u8; 8],
delete: true,
}])
.unwrap();
locked.apply_delete(&[6u8; 8], tags[0]).unwrap();
drop(
locked
.finish(
&mut Vec::new(),
&mut crate::watch::WatchWakeBatch::default(),
)
.unwrap(),
);
let mut again = tree.lock_shards(&[sid]).unwrap();
assert!(
!again
.needs_replay(&[6u8; 8], OpKind::Delete, tags[0])
.unwrap()
);
again.abort();
}
#[test]
fn tx_fixed_replay_skips_the_no_slot_sentinel() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let sid = tree.shard_of(&[7u8; 8]).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tag = VersionTag::Fixed {
slot_id: NO_SLOT,
target_meta: 0,
};
assert!(!locked.needs_replay(&[7u8; 8], OpKind::Delete, tag).unwrap());
locked.abort();
}
#[test]
fn tx_fixed_replay_fails_closed_on_an_unorderable_version() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
tree.put(&[8u8; 8], &[1u8; 16]).unwrap();
let sid = tree.shard_of(&[8u8; 8]).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &[8u8; 8],
delete: false,
}])
.unwrap();
let VersionTag::Fixed {
slot_id,
target_meta,
} = tags[0]
else {
unreachable!("Fixed participant")
};
locked.abort();
let ahead = pack_meta(STATUS_OCCUPIED, version_of(target_meta) + (1 << 29));
tree.tx_debug_force_slot_meta(sid, slot_id, ahead);
let mut again = tree.lock_shards(&[sid]).unwrap();
let err = again
.needs_replay(&[8u8; 8], OpKind::Put, tags[0])
.unwrap_err();
assert!(matches!(err, crate::DbError::TxFixedSlot { .. }), "{err}");
again.abort();
}
#[test]
fn tx_fixed_replay_refuses_a_slot_owned_by_another_key() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let mine = [10u8; 8];
let theirs = [11u8; 8];
let sid = tree.shard_of(&mine).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &mine,
delete: false,
}])
.unwrap();
let VersionTag::Fixed {
slot_id,
target_meta,
} = tags[0]
else {
unreachable!("Fixed participant")
};
locked.abort();
tree.put(&theirs, &[3u8; 16]).unwrap();
let state = tree.tx_debug_slot_state(sid, slot_id).unwrap();
assert_eq!(
state.meta, target_meta,
"the recycled slot reached our exact target version"
);
assert!(state.crc_valid, "the other key's row is intact");
let mut again = tree.lock_shards(&[sid]).unwrap();
let err = again.needs_replay(&mine, OpKind::Put, tags[0]).unwrap_err();
assert!(matches!(err, crate::DbError::TxFixedSlot { .. }), "{err}");
again.abort();
assert_eq!(
tree.get(&theirs),
Some([3u8; 16]),
"the other key's row must be untouched"
);
}
#[test]
fn tx_fixed_replay_skips_an_unorderable_version_when_the_key_moved_on() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let key = [12u8; 8];
let sid = tree.shard_of(&key).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &key,
delete: false,
}])
.unwrap();
let VersionTag::Fixed {
slot_id: orphaned,
target_meta,
} = tags[0]
else {
unreachable!("Fixed participant")
};
drop(
locked
.finish(
&mut Vec::new(),
&mut crate::watch::WatchWakeBatch::default(),
)
.unwrap(),
);
tree.put(&key, &[2u8; 16]).unwrap();
let ahead = pack_meta(STATUS_OCCUPIED, version_of(target_meta) + (1 << 29));
tree.tx_debug_force_slot_meta(sid, orphaned, ahead);
let mut again = tree.lock_shards(&[sid]).unwrap();
assert!(
!again.needs_replay(&key, OpKind::Put, tags[0]).unwrap(),
"a superseded reservation is skipped, not failed closed"
);
again.abort();
assert_eq!(tree.get(&key), Some([2u8; 16]));
}
#[test]
fn tx_fixed_replay_skips_when_key_lands_in_a_newer_slot() {
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let key = [9u8; 8];
let sid = tree.shard_of(&key).unwrap();
let mut locked = tree.lock_shards(&[sid]).unwrap();
let tags = locked
.reserve(&[PlannedOp {
key: &key,
delete: false,
}])
.unwrap();
let VersionTag::Fixed {
slot_id: orphaned, ..
} = tags[0]
else {
unreachable!("Fixed participant")
};
drop(
locked
.finish(
&mut Vec::new(),
&mut crate::watch::WatchWakeBatch::default(),
)
.unwrap(),
);
tree.put(&key, &[2u8; 16]).unwrap();
let live = {
let mut locked = tree.lock_shards(&[sid]).unwrap();
let live_tags = locked
.reserve(&[PlannedOp {
key: &key,
delete: false,
}])
.unwrap();
let VersionTag::Fixed {
slot_id: live_slot, ..
} = live_tags[0]
else {
unreachable!("Fixed participant")
};
locked.abort();
live_slot
};
assert_ne!(
orphaned, live,
"the later write must have allocated a different slot"
);
let mut again = tree.lock_shards(&[sid]).unwrap();
assert!(
!again.needs_replay(&key, OpKind::Put, tags[0]).unwrap(),
"a newer incarnation of the key supersedes the orphaned reservation"
);
again.abort();
assert_eq!(tree.get(&key), Some([2u8; 16]));
}
#[test]
fn tx_fixed_reserve_releases_its_slots_when_a_later_key_fails() {
use crate::fixed::shard::FAIL_RESERVE_IN;
use std::sync::atomic::Ordering;
let dir = tempfile::tempdir().unwrap();
let tree = open_fixed_tree(dir.path(), CountingHook::default());
let sid = tree.shard_of(&[10u8; 8]).unwrap();
let before = tree.tx_debug_occupied_slots(sid);
let keys: [[u8; 8]; 3] = [[10u8; 8], [11u8; 8], [12u8; 8]];
let planned: Vec<PlannedOp<'_>> = keys
.iter()
.map(|k| PlannedOp {
key: k,
delete: false,
})
.collect();
FAIL_RESERVE_IN.store(2, Ordering::SeqCst);
let mut locked = tree.lock_shards(&[sid]).unwrap();
let err = locked.reserve(&planned).unwrap_err();
FAIL_RESERVE_IN.store(usize::MAX, Ordering::SeqCst);
assert!(
format!("{err}").contains("injected reserve failure"),
"{err}"
);
locked.abort();
assert_eq!(
tree.tx_debug_occupied_slots(sid),
before,
"a reserve that failed part-way must leak no slots"
);
}