use std::fmt::Debug;
use std::io;
use std::path::{Path, PathBuf};
use powercut::{
Checker, Coverage, Report, Scenario, ScratchDir, SyncJournal, SyncKind, Tier, Verdict,
Violation,
};
use redb::backends::FileBackend;
use redb::{Builder, Database, Durability, StorageBackend, TableDefinition};
const DB: &str = "app.redb";
const LEDGER: TableDefinition<u64, u64> = TableDefinition::new("ledger");
#[derive(Debug)]
struct JournalingBackend {
inner: FileBackend,
journal: SyncJournal,
path: PathBuf,
}
impl StorageBackend for JournalingBackend {
fn len(&self) -> Result<u64, io::Error> {
self.inner.len()
}
fn read(&self, offset: u64, len: usize) -> Result<Vec<u8>, io::Error> {
self.inner.read(offset, len)
}
fn set_len(&self, len: u64) -> Result<(), io::Error> {
self.inner.set_len(len)
}
fn sync_data(&self, eventual: bool) -> Result<(), io::Error> {
self.inner.sync_data(eventual)?;
if !eventual {
let len = self.inner.len().ok();
self.journal.record(&self.path, SyncKind::Data, len);
}
Ok(())
}
fn write(&self, offset: u64, data: &[u8]) -> Result<(), io::Error> {
self.inner.write(offset, data)
}
}
fn open(dir: &Path, journal: &SyncJournal) -> Database {
let path = dir.join(DB);
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.expect("open redb file");
let backend = JournalingBackend {
inner: FileBackend::new(file).expect("redb file backend"),
journal: journal.clone(),
path,
};
Builder::new()
.create_with_backend(backend)
.expect("open redb database")
}
fn main() {
if cfg!(windows) {
println!(
"redb_backend: SKIPPED on Windows.\n\
redb takes an exclusive whole-file lock while the database is open, and \
powercut captures the store at the ack with the engine still open, so the \
byte copy cannot read it. This is a platform limitation of live capture, \
not a durability finding — see `Capturing a store on Windows` in README.md.\n\
The generic, sqlite and read-fork examples do run on Windows."
);
return;
}
let reports = vec![
durability(Durability::Immediate, "immediate"),
durability(Durability::None, "none"),
durability(Durability::Eventual, "eventual"),
];
let expected = [Verdict::Survived, Verdict::Lost, Verdict::NotMeasured];
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 durability(level: Durability, name: &str) -> Report {
let store = ScratchDir::new(&format!("redb-{name}")).unwrap();
let dir = store.path().to_path_buf();
let journal = if matches!(level, Durability::Eventual) {
SyncJournal::with_coverage(Coverage::partial(
"redb's Durability::Eventual is fcntl(F_BARRIERFSYNC) on macOS — a write barrier, \
not a device-cache flush — and an ordinary fdatasync on Linux. This backend \
credits neither, so an unobserved-but-real flush may have saved this data",
))
} else {
SyncJournal::complete()
};
let db = open(&dir, &journal);
{
let mut txn = db.begin_write().unwrap();
txn.set_durability(Durability::Immediate);
txn.open_table(LEDGER).unwrap();
txn.commit().unwrap();
}
journal.sync_dir(&dir).unwrap();
Scenario::new(&dir, journal.clone())
.label(format!("redb/durability={name}"))
.tier(Tier::PowerCut)
.ack(|| {
let mut txn = db.begin_write().unwrap();
txn.set_durability(level);
{
let mut table = txn.open_table(LEDGER).unwrap();
table.insert(7_u64, 100_u64).unwrap();
}
txn.commit().unwrap(); (7_u64, 100_u64)
})
.unwrap()
.with_teeth_control(|| {
let mut txn = db.begin_write().map_err(io::Error::other)?;
txn.set_durability(Durability::Immediate);
txn.commit().map_err(io::Error::other)?;
journal.sync_dir(&dir)?;
Ok(())
})
.unwrap()
.check(check_ledger)
}
fn check_ledger(crash: &powercut::CrashState, acked: &(u64, u64)) -> Vec<Violation> {
let mut c = Checker::new();
let quiet = SyncJournal::complete(); let db = open(crash.dir(), &quiet);
let txn = db.begin_read().expect("read transaction on the crash copy");
let table = txn.open_table(LEDGER).ok();
c.baseline("the `ledger` table exists", true, table.is_some());
let amount = table.and_then(|t| t.get(acked.0).ok().flatten().map(|v| v.value()));
c.survives("ledger row 7", Some(acked.1), amount);
c.finish()
}