use fusee::core::store::Checkpoint;
use fusee::core::wal::{Progress, WalHeader, WalScanner};
use fusee::{Config, Ctx, Engine, Error};
use std::path::{Path, PathBuf};
use std::time::Duration;
const SHARDS: u32 = 3;
const CHAINS: u32 = 120;
const WAITERS: u32 = 30;
fn tmp(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"fusee-short-log-{name}-{}-{}",
std::process::id(),
fusee::local::unix_ms()
));
let _ = std::fs::remove_dir_all(&dir);
dir
}
fn cfg(dir: &Path) -> Config {
let mut c = Config::local(dir).shards(SHARDS);
c.local.flush = fusee::FlushMethod::Fsync;
c
}
fn chain(engine: &Engine) -> fusee::WorkflowRef<u32, u32> {
engine.register("chain", |ctx: Ctx, n: u32| async move {
let mut sum = 0;
for i in 0..20u32 {
let text = ctx
.step("s", move || async move {
Ok::<_, Error>(format!("{:0>600}", n + i))
})
.await?;
sum += text.parse::<u32>().unwrap_or(0);
}
Ok(sum)
})
}
fn waiter(engine: &Engine) -> fusee::WorkflowRef<u32, u32> {
engine.register("waiter", |ctx: Ctx, n: u32| async move {
let add: u32 = ctx.wait_for("go").await?.unwrap_or(0);
ctx.step("add", move || async move { Ok::<_, Error>(n + add) })
.await
})
}
fn chain_sum(n: u32) -> u32 {
(0..20).map(|i| n + i).sum()
}
async fn fill_and_close(dir: &Path) {
let engine = Engine::open(cfg(dir)).await.unwrap();
let (chain, waiter) = (chain(&engine), waiter(&engine));
for n in 0..WAITERS {
engine.start(&waiter, &format!("w-{n}"), &n).await.unwrap();
}
for n in 0..CHAINS {
let sum = engine.execute(&chain, &format!("c-{n}"), &n).await.unwrap();
assert_eq!(sum, chain_sum(n));
}
engine.shutdown().await.unwrap();
}
async fn check_and_finish(dir: &Path, round: &str) {
let engine = Engine::open(cfg(dir)).await.unwrap();
let (_chain, _waiter) = (chain(&engine), waiter(&engine));
for n in 0..CHAINS {
let sum = engine.run::<u32>(&format!("c-{n}")).result().await;
assert_eq!(sum.unwrap(), chain_sum(n), "{round}: c-{n}");
}
for n in 0..WAITERS {
let run = engine.run::<u32>(&format!("w-{n}"));
run.signal("go", &7u32).await.unwrap();
assert_eq!(run.result().await.unwrap(), n + 7, "{round}: w-{n}");
}
engine.shutdown().await.unwrap();
}
fn chain_of(dir: &Path) -> (usize, u64) {
let (mut segments, mut batches) = (0, 0);
for entry in std::fs::read_dir(dir.join("wal")).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|x| x != "wal") {
continue;
}
let bytes = std::fs::read(&path).unwrap();
if !matches!(WalHeader::decode(&bytes), Ok(Some(_))) {
continue;
}
segments += 1;
let mut scanner = WalScanner::start(&bytes, true).unwrap().unwrap();
match scanner.advance(&bytes, true, bytes.len(), &mut |_| {}) {
Progress::Done(s) => batches += s.batches,
Progress::NeedMore => panic!("a whole segment was read"),
}
}
(segments, batches)
}
fn checkpoints(dir: &Path, shard: u32) -> Vec<(PathBuf, Option<u64>)> {
let mut files: Vec<PathBuf> = std::fs::read_dir(dir.join(format!("shard-{shard:03}")))
.unwrap()
.map(|e| e.unwrap().path())
.filter(|p| p.extension().is_some_and(|x| x == "fck"))
.collect();
files.sort();
files
.into_iter()
.map(|p| {
let lsn = Checkpoint::decode(&std::fs::read(&p).unwrap())
.ok()
.map(|c| c.lsn);
(p, lsn)
})
.collect()
}
fn assert_short_and_covered_twice(dir: &Path, when: &str) {
assert_eq!(
chain_of(dir),
(1, 0),
"{when}: the log's (segments, batches)"
);
for shard in 0..SHARDS {
let cps = checkpoints(dir, shard);
assert_eq!(cps.len(), 2, "{when}: shard {shard} has {cps:?}");
assert!(
cps[0].1.is_some() && cps[0].1 == cps[1].1,
"{when}: shard {shard}'s two checkpoints cover {cps:?}"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_clean_close_leaves_the_log_short_and_every_run_in_place() {
let dir = tmp("close");
fill_and_close(&dir).await;
assert_short_and_covered_twice(&dir, "first close");
Engine::open(cfg(&dir))
.await
.unwrap()
.shutdown()
.await
.unwrap();
assert_short_and_covered_twice(&dir, "idle reopen");
check_and_finish(&dir, "second open").await;
assert_short_and_covered_twice(&dir, "second close");
let engine = Engine::open(cfg(&dir)).await.unwrap();
let _defs = (chain(&engine), waiter(&engine));
for n in 0..WAITERS {
let r = engine.run::<u32>(&format!("w-{n}")).result().await;
assert_eq!(r.unwrap(), n + 7, "third open: w-{n}");
}
engine.shutdown().await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn either_checkpoint_of_a_clean_close_is_enough() {
for damaged in 0..2usize {
let dir = tmp(&format!("damage-{damaged}"));
fill_and_close(&dir).await;
assert_short_and_covered_twice(&dir, "close");
for shard in 0..SHARDS {
let (path, _) = &checkpoints(&dir, shard)[damaged];
let mut bytes = std::fs::read(path).unwrap();
let at = bytes.len() / 2;
bytes[at] ^= 0x5a;
std::fs::write(path, bytes).unwrap();
assert!(checkpoints(&dir, shard)[damaged].1.is_none());
}
Engine::open(cfg(&dir))
.await
.unwrap()
.shutdown()
.await
.unwrap();
assert_short_and_covered_twice(&dir, "idle close after damage");
for shard in 0..SHARDS {
let (path, _) = &checkpoints(&dir, shard)[damaged];
std::fs::write(path, b"not a checkpoint").unwrap();
}
check_and_finish(&dir, &format!("checkpoint {damaged} damaged")).await;
assert_short_and_covered_twice(&dir, "close after damage");
let _ = std::fs::remove_dir_all(&dir);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_log_that_was_not_closed_is_replayed_then_shortened() {
let dir = tmp("unclosed");
{
let engine = Engine::open(cfg(&dir)).await.unwrap();
let (chain, waiter) = (chain(&engine), waiter(&engine));
for n in 0..WAITERS {
engine.start(&waiter, &format!("w-{n}"), &n).await.unwrap();
}
for n in 0..CHAINS {
engine.execute(&chain, &format!("c-{n}"), &n).await.unwrap();
}
let copy = tmp("unclosed-copy");
copy_dir(&dir, ©);
engine.shutdown().await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
std::fs::rename(©, &dir).unwrap();
}
let (_, batches) = chain_of(&dir);
assert!(batches > 0, "the copy holds the log");
let _ = std::fs::remove_file(dir.join("fusee.pid"));
check_and_finish(&dir, "after a crash").await;
assert_short_and_covered_twice(&dir, "close after a crash");
let _ = std::fs::remove_dir_all(&dir);
tokio::time::sleep(Duration::from_millis(1)).await;
}
fn copy_dir(from: &Path, to: &Path) {
std::fs::create_dir_all(to).unwrap();
for entry in std::fs::read_dir(from).unwrap() {
let entry = entry.unwrap();
let target = to.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_dir(&entry.path(), &target);
} else {
std::fs::copy(entry.path(), target).unwrap();
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_waiting_shard_stands_on_two_checkpoint_files_once_the_log_moved_on() {
use fusee::core::shard::shard_of;
let dir = tmp("idle");
let small = |dir: &Path| {
let mut c = Config::local(dir)
.shards(2)
.park_after(Duration::from_secs(600))
.checkpoint_interval(Duration::from_millis(20));
c.local.flush = fusee::FlushMethod::Fsync;
c.local.segment_bytes = 256 << 10;
c
};
let ids = |shard: u32, n: usize| -> Vec<(u32, String)> {
(0u32..)
.map(|k| (k, format!("i-{k}")))
.filter(|(_, id)| shard_of(id, 2) == shard)
.take(n)
.collect()
};
let quiet = ids(1, 9);
let (waiting, finished) = quiet.split_last().unwrap();
let covered_twice = |dir: &Path| {
let cps = checkpoints(dir, 1);
cps.len() == 2 && cps[0].1.is_some() && cps[0].1 == cps[1].1
};
let copy = tmp("idle-copy");
{
let engine = Engine::open(small(&dir)).await.unwrap();
let (chain, waiter) = (chain(&engine), waiter(&engine));
for (n, id) in finished {
assert_eq!(engine.execute(&chain, id, n).await.unwrap(), chain_sum(*n));
}
engine.start(&waiter, &waiting.1, &waiting.0).await.unwrap();
for (n, id) in ids(0, 400) {
engine.execute(&chain, &id, &n).await.unwrap();
if covered_twice(&dir) {
break;
}
}
for _ in 0..200 {
if covered_twice(&dir) {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(
covered_twice(&dir),
"shard 1's checkpoints: {:?}",
checkpoints(&dir, 1)
);
copy_dir(&dir, ©);
engine.shutdown().await.unwrap();
}
let _ = std::fs::remove_file(copy.join("fusee.pid"));
for damaged in 0..2usize {
let trial = tmp(&format!("idle-{damaged}"));
copy_dir(©, &trial);
let (path, _) = &checkpoints(&trial, 1)[damaged];
std::fs::write(path, b"not a checkpoint").unwrap();
let engine = Engine::open(small(&trial)).await.unwrap();
let _defs = (chain(&engine), waiter(&engine));
for (n, id) in finished {
let sum = engine.run::<u32>(id).result().await;
assert_eq!(
sum.unwrap(),
chain_sum(*n),
"checkpoint {damaged} damaged: {id}"
);
}
let run = engine.run::<u32>(&waiting.1);
run.signal("go", &7u32).await.unwrap();
assert_eq!(
run.result().await.unwrap(),
waiting.0 + 7,
"checkpoint {damaged} damaged"
);
engine.shutdown().await.unwrap();
let _ = std::fs::remove_dir_all(&trial);
}
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_dir_all(©);
}