use std::path::{Path, PathBuf};
use powercut::{
Checker, Coverage, DurabilityWitness, Report, Scenario, ScratchDir, SyncJournal, Tier, Verdict,
Violation,
};
use rusqlite::Connection;
const DB: &str = "app.db";
fn main() {
let reports = vec![
synchronous_off(),
durable_ack(),
synchronous_full(),
read_fork_close_tear(),
];
let expected = [
Verdict::Lost,
Verdict::Survived,
Verdict::NotMeasured,
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 synchronous_off() -> Report {
let store = ScratchDir::new("sqlite-off").unwrap();
let dir = store.path().to_path_buf();
let journal = SyncJournal::complete();
provision(&dir, "OFF", &journal);
let conn = open(&dir, "OFF");
Scenario::new(&dir, journal.clone())
.label("sqlite/synchronous=OFF")
.tier(Tier::PowerCut)
.ack(|| {
conn.execute("INSERT INTO ledger(id, amount) VALUES (7, 100)", [])
.unwrap();
(7_i64, 100_i64)
})
.unwrap()
.with_teeth_control(|| durable_ack_now(&dir, &journal))
.unwrap()
.check(check_ledger)
}
fn durable_ack() -> Report {
let store = ScratchDir::new("sqlite-ack").unwrap();
let dir = store.path().to_path_buf();
let journal = SyncJournal::complete();
provision(&dir, "OFF", &journal);
let conn = open(&dir, "OFF");
Scenario::new(&dir, journal.clone())
.label("sqlite/durable-ack")
.tier(Tier::PowerCut)
.ack(|| {
conn.execute("INSERT INTO ledger(id, amount) VALUES (7, 100)", [])
.unwrap();
durable_ack_now(&dir, &journal).unwrap(); (7_i64, 100_i64)
})
.unwrap()
.check(check_ledger)
}
fn synchronous_full() -> Report {
let store = ScratchDir::new("sqlite-full").unwrap();
let dir = store.path().to_path_buf();
let journal = SyncJournal::with_coverage(Coverage::partial(
"libsqlite3 issues its own fsync/F_FULLFSYNC through libc at synchronous=FULL, \
and powercut 0.1 has no syscall-interposition backend to observe them",
));
provision(&dir, "FULL", &journal);
let conn = open(&dir, "FULL");
Scenario::new(&dir, journal.clone())
.label("sqlite/synchronous=FULL")
.tier(Tier::PowerCut)
.ack(|| {
conn.execute("INSERT INTO ledger(id, amount) VALUES (7, 100)", [])
.unwrap();
(7_i64, 100_i64)
})
.unwrap()
.check(check_ledger)
}
fn read_fork_close_tear() -> Report {
let store = ScratchDir::new("sqlite-read-fork").unwrap();
let dir = store.path().to_path_buf();
let journal = SyncJournal::complete();
provision(&dir, "OFF", &journal);
let writer = open(&dir, "OFF");
let reader = open(&dir, "OFF");
writer
.execute("INSERT INTO ledger(id, amount) VALUES (7, 100)", [])
.unwrap();
durable_ack_now(&dir, &journal).unwrap();
drop(writer);
let before = Scenario::new(&dir, journal.clone())
.label("sqlite/read-fork — before the reader closes")
.tier(Tier::PowerCut)
.ack(|| (7_i64, 100_i64))
.unwrap()
.check(check_ledger);
assert_eq!(
before.verdict(),
Verdict::Survived,
"the premise of this case is that the row starts out durable\n{}",
before.to_text()
);
Scenario::new(&dir, journal.clone())
.label("sqlite/read-fork-close-tear")
.tier(Tier::PowerCut)
.ack(|| {
let count: i64 = reader
.query_row("SELECT count(*) FROM ledger", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 1, "the reader really does read the acked row");
drop(reader); (7_i64, 100_i64)
})
.unwrap()
.check(check_ledger)
}
fn open(dir: &Path, synchronous: &str) -> Connection {
let conn = Connection::open(dir.join(DB)).expect("open sqlite");
conn.pragma_update(None, "journal_mode", "WAL")
.expect("WAL mode");
conn.pragma_update(None, "synchronous", synchronous)
.expect("set synchronous");
conn
}
fn provision(dir: &Path, synchronous: &str, journal: &SyncJournal) {
let conn = open(dir, synchronous);
conn.execute_batch("CREATE TABLE ledger(id INTEGER PRIMARY KEY, amount INTEGER);")
.expect("create schema");
conn.execute_batch("PRAGMA wal_checkpoint(FULL);")
.expect("fold the schema into the main file");
drop(conn);
durable_ack_now(dir, journal).expect("make the provisioned schema durable");
}
fn durable_ack_now(dir: &Path, journal: &SyncJournal) -> std::io::Result<()> {
journal.sync_path_if_present(dir.join(DB))?;
journal.sync_path_if_present(dir.join(format!("{DB}-wal")))?;
journal.sync_dir(dir)?;
Ok(())
}
fn check_ledger(crash: &powercut::CrashState, acked: &(i64, i64)) -> Vec<Violation> {
let mut c = Checker::new();
let path: PathBuf = crash.dir().join(DB);
let conn = Connection::open(&path).expect("open the crash copy");
let schema: i64 = conn
.query_row(
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ledger'",
[],
|r| r.get(0),
)
.unwrap_or(0);
c.baseline("the `ledger` table exists", 1_i64, schema);
let amount: Option<i64> = conn
.query_row("SELECT amount FROM ledger WHERE id = ?1", [acked.0], |r| {
r.get(0)
})
.ok();
c.survives("ledger row 7", Some(acked.1), amount);
c.finish()
}
#[allow(dead_code)]
fn coverage_of(journal: &SyncJournal) -> Coverage {
journal.coverage()
}