use std::collections::VecDeque;
use std::error::Error;
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use beamr::atom::{Atom, AtomTable};
use beamr::ets::OwnedTerm;
use beamr::module::ModuleRegistry;
use beamr::native::BifRegistryImpl;
use beamr::scheduler::WasmScheduler;
use beamr::term::Term;
use super::handle::{CommandQueue, ShardCommand, ShardCommandKind, ShardError};
use super::native::{self, ShardNativeHandler};
use crate::db::root_advance::RootAdvanceSeam;
use crate::shard::commit_state::CommitSnapshot;
type TestResult = Result<(), Box<dyn Error>>;
const MAX_TURNS_PER_REPLY: usize = 16;
fn cooperative_scheduler() -> WasmScheduler {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let modules = Arc::new(ModuleRegistry::new());
let bifs = Arc::new(BifRegistryImpl::new());
WasmScheduler::new(atom_table, modules, bifs)
}
fn wake() -> OwnedTerm {
OwnedTerm::immediate(Term::atom(Atom::OK))
}
fn is_dirty(snapshot: &CommitSnapshot) -> bool {
snapshot.recovering
|| snapshot.unreconciled
|| snapshot.committed_gen != snapshot.dirty_gen
|| snapshot.root.is_none()
}
fn enqueue_wake_and_pump<T>(
scheduler: &mut WasmScheduler,
pid: u64,
commands: &CommandQueue,
command: ShardCommand,
reply_rx: &Receiver<T>,
) -> Result<T, Box<dyn Error>> {
native::lock_queue(commands).push_back(command);
scheduler.send_owned(pid, &wake())?;
for _ in 0..MAX_TURNS_PER_REPLY {
let _exited = scheduler.run_native_until_idle();
match reply_rx.try_recv() {
Ok(value) => return Ok(value),
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => {
return Err("shard reply channel disconnected before a reply".into());
}
}
}
Err("shard produced no reply within the cooperative turn budget".into())
}
#[test]
fn ttl_sweep_removal_dirties_the_shard_via_the_cell() -> TestResult {
let mut scheduler = cooperative_scheduler();
let dir = tempfile::tempdir()?;
let store_dir = dir.path().join("store");
let wal_path = dir.path().join("shard.wal");
let commands: CommandQueue = Arc::new(Mutex::new(VecDeque::new()));
let seam = RootAdvanceSeam::new();
let cell = seam.commit_state(0);
let factory = ShardNativeHandler::make_factory(
store_dir,
wal_path,
Arc::clone(&commands),
crate::tree::TreePolicy::V1_DEFAULT,
seam.commit_state(0),
);
let pid = scheduler.spawn_native_root(factory);
let _exited = scheduler.run_native_until_idle();
let (put_tx, put_rx) = mpsc::sync_channel(1);
let put: Result<(), ShardError> = enqueue_wake_and_pump(
&mut scheduler,
pid,
&commands,
ShardCommand {
id: 1,
kind: ShardCommandKind::Put {
key: b"ephemeral".to_vec(),
value: b"v".to_vec(),
ttl: Some(Duration::ZERO),
reply: put_tx,
},
},
&put_rx,
)?;
put?;
let (commit_tx, commit_rx) = mpsc::sync_channel(1);
let committed: Result<(crate::tree::Hash, _), ShardError> = enqueue_wake_and_pump(
&mut scheduler,
pid,
&commands,
ShardCommand {
id: 2,
kind: ShardCommandKind::Commit { reply: commit_tx },
},
&commit_rx,
)?;
committed?;
assert!(
!is_dirty(&cell.snapshot()),
"the shard is CLEAN after committing the put (setup precondition)"
);
let (sweep_tx, sweep_rx) = mpsc::sync_channel(1);
let removed: Result<bool, ShardError> = enqueue_wake_and_pump(
&mut scheduler,
pid,
&commands,
ShardCommand {
id: 3,
kind: ShardCommandKind::DeleteIfExpired {
key: b"ephemeral".to_vec(),
reply: sweep_tx,
},
},
&sweep_rx,
)?;
assert!(
removed?,
"the expired key is swept (buffers a removal tombstone)"
);
assert!(
is_dirty(&cell.snapshot()),
"a TTL sweep that buffers a removal marks the shard DIRTY via the cell (§2.1)"
);
let (commit2_tx, commit2_rx) = mpsc::sync_channel(1);
let recommitted: Result<(crate::tree::Hash, _), ShardError> = enqueue_wake_and_pump(
&mut scheduler,
pid,
&commands,
ShardCommand {
id: 4,
kind: ShardCommandKind::Commit { reply: commit2_tx },
},
&commit2_rx,
)?;
recommitted?;
assert!(
!is_dirty(&cell.snapshot()),
"committing the swept removal returns the shard to clean"
);
Ok(())
}