powercut 0.1.1

Prove an acknowledged write survives a power cut or process crash, in cargo test. A database-agnostic crash-injection harness that derives the durable set from the fsyncs that actually happened.
Documentation
//! `powercut` against **redb**, observed through the engine's own
//! `StorageBackend` trait — a pure-Rust engine measured with **complete**
//! coverage and no interposition anywhere.
//!
//! ```console
//! cargo run --example redb_backend
//! ```
//!
//! This is the best case for `powercut`, and worth copying if your engine
//! exposes a storage-layer trait: every byte redb writes and every sync it
//! performs goes through [`JournalingBackend`], so the durable set is not a
//! model of what redb does — it is a recording of it.
//!
//! Three cases, one per `redb::Durability`:
//!
//! | case | durability | verdict | why |
//! |---|---|---|---|
//! | `redb/durability=immediate` | `Immediate` | **survived** | a real `fsync` before `commit()` returns |
//! | `redb/durability=none` | `None` | **lost** | no sync at all; the commit is a promise about memory |
//! | `redb/durability=eventual` | `Eventual` | **not-measured** | see below |
//!
//! `Eventual` is the interesting one and it is a genuinely good example of why
//! `Finding::NotMeasured` has to exist. On macOS redb implements it as
//! `fcntl(F_BARRIERFSYNC)` — a write **barrier**, which orders writes without
//! flushing the device cache — while on Linux the same call is an ordinary
//! `fdatasync`, which is a real flush. One `Durability` value, two different
//! durability guarantees. This backend refuses to credit either, declares
//! `Coverage::partial` saying exactly that, and `powercut` therefore reports
//! `NotMeasured` rather than picking a side.

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");

// ── The adapter: ~25 lines, and this is the whole integration ────────────

/// Wraps redb's own [`FileBackend`] and records every **credited** sync into a
/// [`SyncJournal`].
///
/// Only `eventual == false` syncs are recorded, because those are the ones
/// that are a real device flush on every platform redb supports. See the
/// module docs for why the other kind is deliberately not credited.
#[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> {
        // The sync happens FIRST. The journal is only ever told about syncs
        // that returned Ok — it must mean "this is on stable storage", never
        // "we tried".
        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() {
    // On Windows redb holds an EXCLUSIVE whole-file lock (`LockFileEx`) for as
    // long as the database is open, and a byte range locked that way cannot be
    // read through any other handle. `powercut` captures the store *while the
    // engine is still open* — that is the entire point of the measurement — so
    // the copy fails with ERROR_LOCK_VIOLATION and no measurement is possible.
    //
    // Note what `powercut` does NOT do here: it does not skip the unreadable
    // file and carry on. A store file silently missing from a capture would
    // manufacture a false `Lost`, which is the exact failure mode this crate
    // exists to avoid. It returns an error, and this example declines to run
    // rather than turning that error into a panic.
    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()
    };

    // Provision: create the table and make it durable BEFORE the measured
    // write, so a red reads "the measured write was lost" rather than the far
    // less useful "the database was never on disk".
    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(); // ← the ack: redb says "committed"
            (7_u64, 100_u64)
        })
        .unwrap()
        // The teeth control differs from the measured run by exactly one
        // thing: the durability level. Note that it is NOT an fsync of the
        // file — at `Durability::None` redb holds the commit in user space,
        // and no amount of fsync makes memory durable. The flush a teeth
        // control needs is whatever YOUR system's "really persist it" is.
        .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(); // the reboot's own syncs are not the measurement
    let db = open(crash.dir(), &quiet);
    let txn = db.begin_read().expect("read transaction on the crash copy");

    // Baseline first: no table means the copy is not a usable database, and
    // nothing below it would be a durability measurement.
    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()
}