use super::{Ballot, ShardActor};
use crate::store::MemoryStore;
use crate::sync::topology::SyncNodeId;
use crate::wal::{DurableWal, FsyncPolicy, PROMISE_COMPACTION_THRESHOLD, WalError, WalRecovery};
use std::error::Error;
use std::path::{Path, PathBuf};
type BoxError = Box<dyn Error>;
struct TempWal {
_dir: tempfile::TempDir,
path: PathBuf,
}
fn temp_wal() -> Result<TempWal, WalError> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("shard.wal");
Ok(TempWal { _dir: dir, path })
}
fn ballot(counter: u64) -> Ballot {
Ballot::new(counter, SyncNodeId::from("owner"))
}
fn reopen(path: &Path, store: &MemoryStore) -> Result<ShardActor, WalError> {
let recovered = WalRecovery::recover_path(path, store)?;
let wal = DurableWal::new(path, FsyncPolicy::CommitOnly)?;
ShardActor::from_recovered(wal, recovered, store, crate::tree::TreePolicy::V1_DEFAULT)
}
fn frame_count(path: &Path) -> Result<usize, BoxError> {
let bytes = std::fs::read(path)?;
let mut offset = 0_usize;
let mut frames = 0_usize;
while offset < bytes.len() {
let end = offset.checked_add(4).ok_or("frame length overflow")?;
let len_bytes = bytes.get(offset..end).ok_or("truncated frame length")?;
let len =
u32::from_le_bytes([len_bytes[0], len_bytes[1], len_bytes[2], len_bytes[3]]) as usize;
offset = end.checked_add(len).ok_or("frame body overflow")?;
frames = frames.saturating_add(1);
}
Ok(frames)
}
#[test]
fn promise_churn_on_markerless_shard_stays_bounded_and_recovers() -> Result<(), BoxError> {
let temp = temp_wal()?;
let appends = PROMISE_COMPACTION_THRESHOLD + 10;
{
let wal = DurableWal::new(&temp.path, FsyncPolicy::CommitOnly)?;
let mut actor = ShardActor::new(wal);
for counter in 1..=appends as u64 {
actor.record_promise(ballot(counter))?;
}
drop(actor);
}
let frames = frame_count(&temp.path)?;
assert!(
frames <= PROMISE_COMPACTION_THRESHOLD + 1,
"WAL bounded at marker+threshold+1: {frames} frames after {appends} appends"
);
assert!(
frames < appends,
"compaction must have fired ({frames} frames < {appends} appends)"
);
let store = MemoryStore::new();
let recovered = reopen(&temp.path, &store)?;
assert_eq!(
recovered.committed_root(),
None,
"a marker-less compacted shard recovers with no committed root"
);
assert_eq!(
recovered.promised(),
&ballot(appends as u64),
"the latest promise survives compaction across recovery"
);
Ok(())
}
#[test]
fn promise_churn_on_markerbearing_shard_preserves_root_and_promise() -> Result<(), BoxError> {
let temp = temp_wal()?;
let appends = PROMISE_COMPACTION_THRESHOLD + 10;
let mut store = MemoryStore::new();
let committed_root;
{
let wal = DurableWal::new(&temp.path, FsyncPolicy::CommitOnly)?;
let mut actor = ShardActor::new(wal);
actor.put_with_ttl(b"k".to_vec(), b"v".to_vec(), None, &store)?;
committed_root = actor.commit(&mut store)?;
for counter in 1..=appends as u64 {
actor.record_promise(ballot(counter))?;
}
drop(actor);
}
let frames = frame_count(&temp.path)?;
assert!(
frames <= PROMISE_COMPACTION_THRESHOLD + 2,
"WAL bounded (marker + threshold + 1): {frames} frames after {appends} appends"
);
let recovered = reopen(&temp.path, &store)?;
assert_eq!(
recovered.committed_root(),
Some(committed_root),
"the committed root survives marker-bearing promise compaction"
);
assert_eq!(
recovered.promised(),
&ballot(appends as u64),
"the latest promise survives marker-bearing compaction across recovery"
);
Ok(())
}