use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use powercut::{Checker, Report, Scenario, ScratchDir, SyncJournal, Tier, Verdict, Violation};
struct LogStore {
log: PathBuf,
file: File,
}
impl LogStore {
fn open(dir: &Path) -> std::io::Result<Self> {
let log = dir.join("store.log");
let file = OpenOptions::new().create(true).append(true).open(&log)?;
Ok(Self { log, file })
}
fn put(&mut self, key: &str, value: &str) -> std::io::Result<()> {
writeln!(self.file, "{key}={value}")?;
self.file.flush() }
fn get(dir: &Path, key: &str) -> Option<String> {
let mut found = None;
for path in [dir.join("store.base"), dir.join("store.log")] {
let Ok(file) = File::open(&path) else {
continue;
};
for line in BufReader::new(file).lines().map_while(Result::ok) {
if let Some((k, v)) = line.split_once('=') {
if k == key {
found = Some(v.to_string());
}
}
}
}
found
}
fn compact_on_close(dir: &Path, journal: &SyncJournal) -> std::io::Result<()> {
let log = dir.join("store.log");
let base = dir.join("store.base");
let mut merged = std::fs::read_to_string(&base).unwrap_or_default();
merged.push_str(&std::fs::read_to_string(&log).unwrap_or_default());
std::fs::write(&base, merged)?; std::fs::remove_file(&log)?;
journal.sync_dir(dir)?; Ok(())
}
}
fn main() {
let reports = vec![
commit_without_fsync(),
durable_ack(),
read_fork_close_tear(),
];
let expected = [Verdict::Lost, Verdict::Survived, Verdict::Lost];
for report in &reports {
println!("{}", report.to_text());
}
println!("{}", powercut::to_markdown(&reports));
for (report, expected) in reports.iter().zip(expected) {
assert_eq!(
report.verdict(),
expected,
"`{}` was expected to be {expected}\n{}",
report.label(),
report.to_text()
);
}
println!("all {} cases behaved as documented", reports.len());
}
fn commit_without_fsync() -> Report {
let store = ScratchDir::new("generic-no-fsync").unwrap();
let journal = SyncJournal::complete();
let dir = store.path().to_path_buf();
Scenario::new(&dir, journal.clone())
.label("generic/commit-without-fsync")
.tier(Tier::PowerCut)
.ack(|| {
let mut s = LogStore::open(&dir).unwrap();
s.put("balance", "100").unwrap();
"100"
})
.unwrap()
.with_teeth_control(|| {
journal.sync_path(dir.join("store.log"))?;
journal.sync_dir(&dir)?;
Ok(())
})
.unwrap()
.check(check_balance)
}
fn durable_ack() -> Report {
let store = ScratchDir::new("generic-durable-ack").unwrap();
let journal = SyncJournal::complete();
let dir = store.path().to_path_buf();
Scenario::new(&dir, journal.clone())
.label("generic/durable-ack")
.tier(Tier::PowerCut)
.ack(|| {
let mut s = LogStore::open(&dir).unwrap();
s.put("balance", "100").unwrap();
journal.sync_all(&s.file, &s.log).unwrap(); journal.sync_dir(&dir).unwrap(); "100"
})
.unwrap()
.check(check_balance)
}
fn read_fork_close_tear() -> Report {
let store = ScratchDir::new("generic-read-fork").unwrap();
let journal = SyncJournal::complete();
let dir = store.path().to_path_buf();
{
let mut s = LogStore::open(&dir).unwrap();
s.put("balance", "100").unwrap();
journal.sync_all(&s.file, &s.log).unwrap();
journal.sync_dir(&dir).unwrap();
}
Scenario::new(&dir, journal.clone())
.label("generic/read-fork-close-tear")
.tier(Tier::PowerCut)
.ack(|| {
let _reader = LogStore::get(&dir, "balance");
LogStore::compact_on_close(&dir, &journal).unwrap();
"100"
})
.unwrap()
.with_teeth_control(|| {
journal.sync_path_if_present(dir.join("store.base"))?;
journal.sync_dir(&dir)?;
Ok(())
})
.unwrap()
.check(check_balance)
}
fn check_balance(crash: &powercut::CrashState, acked: &&str) -> Vec<Violation> {
let mut c = Checker::new();
c.survives(
"balance",
Some((*acked).to_string()),
LogStore::get(crash.dir(), "balance"),
);
c.finish()
}