use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use fathomdb_engine::{Engine, PreparedWrite};
use tempfile::TempDir;
fn long_run_enabled() -> bool {
std::env::var_os("AGENT_LONG").is_some()
}
const P_PWR_TRIALS: usize = 100;
const AC_034B_P99_BUDGET_MS: u128 = 100;
const PWR_MIN_KILL_MS: u64 = 30;
const PWR_MAX_KILL_MS: u64 = 180;
const VICTIM_REAP_BUDGET: Duration = Duration::from_secs(5);
const SENTINEL_WAIT_BUDGET: Duration = Duration::from_secs(5);
#[test]
#[ignore = "test-binary-as-victim entry-point for the AC-034a/b power-cut harness"]
fn _power_cut_victim_entry() {
let Some(db_path_os) = std::env::var_os("FATHOMDB_POWER_CUT_VICTIM_DB") else {
return;
};
let path = PathBuf::from(db_path_os);
let sentinel = std::env::var_os("FATHOMDB_POWER_CUT_VICTIM_SENTINEL").map(PathBuf::from);
let opened = Engine::open(&path).expect("victim engine open");
let mut first = true;
loop {
let body = micros_since_epoch().to_string();
opened
.engine
.write(&[PreparedWrite::Node { kind: "doc".to_string(), body, source_id: None }])
.expect("victim write");
if first {
if let Some(path) = sentinel.as_ref() {
std::fs::File::create(path).expect("victim sentinel create");
}
first = false;
}
}
}
fn micros_since_epoch() -> u128 {
SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_micros()
}
#[test]
fn ac_034a_and_b_power_cut_zero_corruption_and_p99_lost_commit() {
if !long_run_enabled() {
eprintln!("AC-034a/b skipped (AGENT_LONG not set)");
return;
}
let outcomes = run_power_cut_trials(P_PWR_TRIALS);
assert_eq!(
outcomes.len(),
P_PWR_TRIALS,
"AC-034b: harness collected {} trials, expected {}",
outcomes.len(),
P_PWR_TRIALS,
);
let bad: Vec<_> = outcomes.iter().filter(|o| o.integrity != "ok").collect();
assert!(
bad.is_empty(),
"AC-034a: {} of {} trials returned non-ok integrity_check: {:?}",
bad.len(),
outcomes.len(),
bad,
);
let mut lost: Vec<u128> = outcomes.iter().map(|o| o.lost_commit_ms).collect();
lost.sort_unstable();
let p99_index = ((lost.len() as f64 * 0.99).ceil() as usize).saturating_sub(1);
let p99 = lost[p99_index];
eprintln!("AC-034a: integrity_check ok on {}/{} trials", outcomes.len(), outcomes.len());
eprintln!(
"AC-034b: lost-commit ms — n={}, min={}, median={}, p99={}",
lost.len(),
lost[0],
lost[lost.len() / 2],
p99,
);
assert!(
p99 <= AC_034B_P99_BUDGET_MS,
"AC-034b: lost-commit p99 = {} ms > {} ms budget",
p99,
AC_034B_P99_BUDGET_MS,
);
}
#[derive(Debug)]
struct TrialOutcome {
integrity: String,
lost_commit_ms: u128,
}
fn run_power_cut_trials(trials: usize) -> Vec<TrialOutcome> {
let exe = std::env::current_exe().expect("current_exe");
let mut outcomes = Vec::with_capacity(trials);
for trial in 0..trials {
let dir = TempDir::new().expect("tempdir");
let db_path = dir.path().join("power-cut.sqlite");
let sentinel_path = dir.path().join("first-commit.sentinel");
let mut child = Command::new(&exe)
.args(["--exact", "--ignored", "_power_cut_victim_entry"])
.env("FATHOMDB_POWER_CUT_VICTIM_DB", &db_path)
.env("FATHOMDB_POWER_CUT_VICTIM_SENTINEL", &sentinel_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn victim");
let pid = child.id() as i32;
wait_for_sentinel(&sentinel_path, SENTINEL_WAIT_BUDGET, pid, trial);
let sleep_ms = trial_sleep_ms(trial);
std::thread::sleep(Duration::from_millis(sleep_ms));
let kill_micros = micros_since_epoch();
let kill_rc = unsafe { libc::kill(pid, libc::SIGKILL) };
if kill_rc != 0 {
panic!("trial {trial}: SIGKILL failed: {}", std::io::Error::last_os_error());
}
wait_with_budget(&mut child, VICTIM_REAP_BUDGET, trial);
let last_commit_micros = read_last_commit_micros(&db_path).unwrap_or_else(|| {
panic!(
"trial {trial}: sentinel landed but no committed row \
recovered after SIGKILL — open path lost a durably \
committed write"
)
});
let integrity = run_integrity_check(&db_path);
let lost_commit_ms = kill_micros.saturating_sub(last_commit_micros) / 1_000;
outcomes.push(TrialOutcome { integrity, lost_commit_ms });
}
outcomes
}
fn wait_for_sentinel(sentinel: &std::path::Path, budget: Duration, pid: i32, trial: usize) {
let started = Instant::now();
while !sentinel.exists() {
if started.elapsed() > budget {
let _ = unsafe { libc::kill(pid, libc::SIGKILL) };
panic!(
"trial {trial}: victim did not land its first commit \
within {budget:?}; AC-034b full-N p99 contract requires \
every trial to commit at least once before SIGKILL"
);
}
std::thread::sleep(Duration::from_millis(1));
}
}
fn trial_sleep_ms(trial: usize) -> u64 {
let mixed = (trial as u64)
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let range = PWR_MAX_KILL_MS - PWR_MIN_KILL_MS;
PWR_MIN_KILL_MS + (mixed % range)
}
fn wait_with_budget(child: &mut std::process::Child, budget: Duration, trial: usize) {
let started = Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => return,
Ok(None) => {
if started.elapsed() > budget {
panic!("trial {trial}: victim did not exit within {:?} after SIGKILL", budget);
}
std::thread::sleep(Duration::from_millis(5));
}
Err(err) => panic!("trial {trial}: wait failed: {err}"),
}
}
}
fn read_last_commit_micros(db_path: &std::path::Path) -> Option<u128> {
let opened = Engine::open(db_path).expect("post-kill engine open");
let path = opened.engine.path().to_path_buf();
opened.engine.close().expect("post-kill close");
drop(opened);
let conn = rusqlite::Connection::open(&path).expect("post-kill rusqlite open");
let max: Option<String> = conn
.query_row(
"SELECT body FROM canonical_nodes
WHERE kind = 'doc'
ORDER BY CAST(body AS INTEGER) DESC LIMIT 1",
[],
|row| row.get(0),
)
.ok();
max.and_then(|raw| raw.parse::<u128>().ok())
}
fn run_integrity_check(db_path: &std::path::Path) -> String {
let conn = rusqlite::Connection::open(db_path).expect("integrity rusqlite open");
conn.query_row("PRAGMA integrity_check", [], |row| row.get::<_, String>(0))
.unwrap_or_else(|err| format!("integrity_check query failed: {err}"))
}
#[test]
#[ignore = "AC-034c blocked on missing VM/sysrq fixture; see Phase 12-D \
output JSON (blocker-3). Do NOT clear the #[ignore] until \
the VM image lands in dev/test-plan.md."]
fn ac_034c_os_crash_zero_committed_tx_loss() {
panic!(
"AC-034c requires a KVM image with `echo c > /proc/sysrq-trigger` \
and a preserved disk sync barrier (per `dev/acceptance.md` § \
AC-034c fixture). That VM substrate does not exist in this repo. \
See `dev/plans/runs/12-D-durability-harnesses-output.json` \
blocker-3 for the substrate-gap detail and the recommended \
12-D-OS-CRASH follow-up slice."
);
}