#![forbid(unsafe_code)]
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use noxu_util::Prng;
static ACTIVE: AtomicBool = AtomicBool::new(false);
static WRITE_COUNT: AtomicU64 = AtomicU64::new(0);
static CONTROLLER: Mutex<Option<FaultController>> = Mutex::new(None);
pub fn write_count() -> u64 {
WRITE_COUNT.load(Ordering::SeqCst)
}
#[inline]
pub fn is_active() -> bool {
ACTIVE.load(Ordering::Relaxed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaultKind {
None,
TornWrite,
FsyncDrop,
DiskFull,
Corruption,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteFault {
None,
Torn(usize),
DiskFull,
Corrupt { offset_in_buf: usize, len: usize },
}
#[derive(Debug)]
pub struct FaultController {
kind: FaultKind,
target_write: u64,
magnitude: u64,
prng: Prng,
fired: bool,
}
impl FaultController {
pub fn from_seed(seed: u64) -> Self {
let mut prng = Prng::new(seed);
let kind = match prng.below(5) {
0 => FaultKind::None,
1 => FaultKind::TornWrite,
2 => FaultKind::FsyncDrop,
3 => FaultKind::DiskFull,
_ => FaultKind::Corruption,
};
let target_write = prng.below(64);
let magnitude = 1 + prng.below(15); FaultController { kind, target_write, magnitude, prng, fired: false }
}
pub fn kind(&self) -> FaultKind {
self.kind
}
pub fn target_write(&self) -> u64 {
self.target_write
}
pub fn fired(&self) -> bool {
self.fired
}
}
pub fn install(controller: FaultController) {
*CONTROLLER.lock().expect("faultdisk mutex") = Some(controller);
ACTIVE.store(true, Ordering::SeqCst);
}
pub fn install_seed(seed: u64) {
install(FaultController::from_seed(seed));
}
pub fn uninstall() {
ACTIVE.store(false, Ordering::SeqCst);
*CONTROLLER.lock().expect("faultdisk mutex") = None;
WRITE_COUNT.store(0, Ordering::SeqCst);
}
pub fn on_write(buf_len: usize) -> WriteFault {
if !is_active() {
return WriteFault::None;
}
let idx = WRITE_COUNT.fetch_add(1, Ordering::SeqCst);
let mut guard = CONTROLLER.lock().expect("faultdisk mutex");
let Some(ctrl) = guard.as_mut() else {
return WriteFault::None;
};
if idx != ctrl.target_write {
return WriteFault::None;
}
match ctrl.kind {
FaultKind::TornWrite => {
let keep = ((buf_len as u64 * ctrl.magnitude) / 16) as usize;
let keep = keep.min(buf_len.saturating_sub(1));
ctrl.fired = true;
WriteFault::Torn(keep)
}
FaultKind::DiskFull => WriteFault::DiskFull,
FaultKind::Corruption => {
let len = (ctrl.magnitude as usize).min(buf_len);
WriteFault::Corrupt { offset_in_buf: 0, len }
}
FaultKind::FsyncDrop | FaultKind::None => WriteFault::None,
}
}
pub fn on_fsync() -> bool {
if !is_active() {
return false;
}
let mut guard = CONTROLLER.lock().expect("faultdisk mutex");
let Some(ctrl) = guard.as_mut() else {
return false;
};
if ctrl.kind == FaultKind::FsyncDrop && !ctrl.fired {
if WRITE_COUNT.load(Ordering::SeqCst) >= ctrl.target_write {
ctrl.fired = true;
return true;
}
}
false
}
pub fn power_cut() -> ! {
eprintln!("[faultdisk] simulated power loss (process exit)");
std::process::exit(137);
}
#[cfg(test)]
mod tests {
use super::*;
static GUARD: Mutex<()> = Mutex::new(());
#[test]
fn inactive_by_default_zero_cost_path() {
let _g = GUARD.lock().unwrap();
uninstall();
assert!(!is_active());
assert_eq!(on_write(4096), WriteFault::None);
assert!(!on_fsync());
}
#[test]
fn same_seed_same_plan() {
let a = FaultController::from_seed(12345);
let b = FaultController::from_seed(12345);
assert_eq!(a.kind(), b.kind());
assert_eq!(a.target_write(), b.target_write());
}
#[test]
fn torn_write_fires_at_target() {
let _g = GUARD.lock().unwrap();
let mut seed = 0u64;
let ctrl = loop {
let c = FaultController::from_seed(seed);
if c.kind() == FaultKind::TornWrite && c.target_write() < 5 {
break c;
}
seed += 1;
};
let target = ctrl.target_write();
install(ctrl);
for _ in 0..target {
assert_eq!(on_write(4096), WriteFault::None);
}
match on_write(4096) {
WriteFault::Torn(keep) => assert!(keep < 4096),
other => panic!("expected Torn, got {other:?}"),
}
uninstall();
}
#[test]
fn disk_full_fires_at_target() {
let _g = GUARD.lock().unwrap();
let mut seed = 0u64;
let ctrl = loop {
let c = FaultController::from_seed(seed);
if c.kind() == FaultKind::DiskFull && c.target_write() < 3 {
break c;
}
seed += 1;
};
let target = ctrl.target_write();
install(ctrl);
for _ in 0..target {
assert_eq!(on_write(100), WriteFault::None);
}
assert_eq!(on_write(100), WriteFault::DiskFull);
uninstall();
}
}