use std::env;
use std::fs;
use std::path::Path;
use std::process;
use chronon::engine::format::GENESIS_HASH;
use chronon::engine::log::LogWriter;
use chronon::engine::recovery::{LogRecovery, RecoveryOutcome};
use tracing::info;
use tracing_subscriber::EnvFilter;
const LOG_PATH: &str = "/tmp/chr_crash_test.log";
const ENTRIES_PER_RUN: u64 = 100;
fn main() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive("chr=info".parse().unwrap()))
.with_target(true)
.with_thread_ids(true)
.init();
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
match args[1].as_str() {
"clean" => {
let _ = fs::remove_file(LOG_PATH);
println!("Cleaned log file.");
return;
}
"write" => {
run_write_phase();
return;
}
"recover" => {
run_recovery_phase();
return;
}
"test" => {
run_crash_test();
return;
}
_ => {
print_usage();
return;
}
}
}
run_crash_test();
}
fn print_usage() {
eprintln!("Usage: chr [command]");
eprintln!("Commands:");
eprintln!(" clean - Remove existing log file");
eprintln!(" write - Append entries (may crash randomly)");
eprintln!(" recover - Run recovery and report state");
eprintln!(" test - Full crash test cycle");
eprintln!(" (none) - Same as 'test'");
}
fn run_write_phase() {
let path = Path::new(LOG_PATH);
let (next_index, write_offset, tail_hash, view_id) = match LogRecovery::open(path) {
Ok(Some(recovery)) => {
match recovery.scan() {
Ok(outcome) => match outcome {
RecoveryOutcome::CleanEmpty { .. } => (0, 0, GENESIS_HASH, 1),
RecoveryOutcome::Clean {
last_index,
next_offset,
tail_hash,
highest_view,
..
} => (last_index + 1, next_offset, tail_hash, highest_view),
RecoveryOutcome::Truncated {
last_valid_index,
new_offset,
tail_hash,
highest_view,
..
} => {
if last_valid_index == 0 && new_offset == 0 {
(0, 0, GENESIS_HASH, 1)
} else {
(last_valid_index + 1, new_offset, tail_hash, highest_view)
}
}
},
Err(e) => {
eprintln!("FATAL: Recovery failed: {}", e);
process::exit(1);
}
}
}
Ok(None) => (0, 0, GENESIS_HASH, 1),
Err(e) => {
eprintln!("FATAL: Failed to open log: {}", e);
process::exit(1);
}
};
println!(
"Starting write phase: next_index={}, offset={}",
next_index, write_offset
);
let mut writer = match LogWriter::open(path, next_index, write_offset, tail_hash, view_id) {
Ok(w) => w,
Err(e) => {
eprintln!("FATAL: Failed to open log writer: {}", e);
process::exit(1);
}
};
let crash_after = get_crash_point(next_index);
for i in 0..ENTRIES_PER_RUN {
let payload = format!("entry-{}-{}", next_index + i, std::process::id());
if i == crash_after {
println!("SIMULATING CRASH at entry {} (before fdatasync)", next_index + i);
process::exit(0);
}
let timestamp_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
match writer.append(payload.as_bytes(), 0, 0, timestamp_ns) {
Ok(idx) => {
if i % 10 == 0 {
println!("Wrote entry {}", idx);
}
}
Err(e) => {
eprintln!("FATAL: Write failed at index {}: {}", next_index + i, e);
process::exit(1);
}
}
}
println!(
"Write phase complete. Last index: {}",
writer.next_index() - 1
);
}
fn get_crash_point(next_index: u64) -> u64 {
let pid = std::process::id() as u64;
let seed = next_index.wrapping_add(pid);
let hash = seed.wrapping_mul(2654435761) % ENTRIES_PER_RUN;
if hash < ENTRIES_PER_RUN * 2 / 5 {
(hash.wrapping_mul(7)) % ENTRIES_PER_RUN
} else {
ENTRIES_PER_RUN + 1 }
}
fn run_recovery_phase() {
let path = Path::new(LOG_PATH);
match LogRecovery::open(path) {
Ok(Some(recovery)) => {
println!("Running recovery scan...");
match recovery.scan() {
Ok(outcome) => {
match outcome {
RecoveryOutcome::CleanEmpty { .. } => {
println!("Recovery: CLEAN (empty log)");
println!("Last valid index: none");
}
RecoveryOutcome::Clean {
last_index,
next_offset,
highest_view,
..
} => {
println!("Recovery: CLEAN");
println!("Last valid index: {}", last_index);
println!("Next offset: {}", next_offset);
println!("Highest view: {}", highest_view);
}
RecoveryOutcome::Truncated {
last_valid_index,
truncated_at,
new_offset,
highest_view,
..
} => {
println!("Recovery: TRUNCATED (torn write repaired)");
println!("Last valid index: {}", last_valid_index);
println!("Truncated at offset: {}", truncated_at);
println!("New write offset: {}", new_offset);
println!("Highest view: {}", highest_view);
}
}
process::exit(0);
}
Err(e) => {
eprintln!("FATAL CORRUPTION: {}", e);
process::exit(1);
}
}
}
Ok(None) => {
println!("No log file found.");
process::exit(0);
}
Err(e) => {
eprintln!("FATAL: Failed to open log: {}", e);
process::exit(1);
}
}
}
fn run_crash_test() {
println!("=== chr Crash Test Harness ===\n");
let path = Path::new(LOG_PATH);
println!("Phase 1: Cleaning previous state...");
let _ = fs::remove_file(path);
println!("\nPhase 2: Writing entries...");
for round in 0..5 {
println!("\n--- Round {} ---", round + 1);
let status = run_write_subprocess();
if !status.success() {
eprintln!("Write subprocess failed with non-zero exit");
process::exit(1);
}
println!("\nRunning recovery...");
match LogRecovery::open(path) {
Ok(Some(recovery)) => match recovery.scan() {
Ok(outcome) => {
print_outcome(&outcome);
}
Err(e) => {
eprintln!("FATAL CORRUPTION: {}", e);
process::exit(1);
}
},
Ok(None) => {
println!("No log file (expected after clean)");
}
Err(e) => {
eprintln!("FATAL: Failed to open log: {}", e);
process::exit(1);
}
}
}
println!("\n=== Crash Test Complete ===");
println!("All rounds passed. Log integrity verified.");
}
fn run_write_subprocess() -> process::ExitStatus {
let exe = env::current_exe().expect("Failed to get current executable");
process::Command::new(exe)
.arg("write")
.status()
.expect("Failed to run write subprocess")
}
fn print_outcome(outcome: &RecoveryOutcome) {
match outcome {
RecoveryOutcome::CleanEmpty { .. } => {
println!(" Status: CLEAN (empty)");
}
RecoveryOutcome::Clean { last_index, .. } => {
println!(" Status: CLEAN");
println!(" Last valid index: {}", last_index);
}
RecoveryOutcome::Truncated {
last_valid_index,
truncated_at,
..
} => {
println!(" Status: TRUNCATED (torn write repaired)");
println!(" Last valid index: {}", last_valid_index);
println!(" Truncated at: {}", truncated_at);
}
}
}