use crate::storage::StorageBackend;
use anyhow::Result;
use std::io;
use std::sync::{Arc, Mutex};
#[derive(Debug, Default)]
pub struct FaultConfig {
pub fail_write_after: Option<u64>,
pub fail_sync_after: Option<u64>,
pub fail_close_after: Option<u64>,
write_count: u64,
sync_count: u64,
close_count: u64,
}
impl FaultConfig {
fn check_and_increment(count: &mut u64, limit: Option<u64>) -> Result<()> {
if limit.is_some_and(|n| *count >= n) {
return Err(anyhow::Error::new(io::Error::other(
"fault injection: simulated I/O error",
)));
}
*count += 1;
Ok(())
}
}
pub struct FaultInjectingBackend<B: StorageBackend> {
inner: B,
config: Arc<Mutex<FaultConfig>>,
}
impl<B: StorageBackend> FaultInjectingBackend<B> {
pub fn with_config(inner: B) -> (Self, Arc<Mutex<FaultConfig>>) {
let config = Arc::new(Mutex::new(FaultConfig::default()));
let backend = FaultInjectingBackend {
inner,
config: config.clone(),
};
(backend, config)
}
}
impl<B: StorageBackend> StorageBackend for FaultInjectingBackend<B> {
fn write_page(&mut self, page_id: u64, data: &[u8]) -> Result<()> {
let mut cfg = self.config.lock().unwrap();
let limit = cfg.fail_write_after;
FaultConfig::check_and_increment(&mut cfg.write_count, limit)?;
drop(cfg);
self.inner.write_page(page_id, data)
}
fn read_page(&self, page_id: u64) -> Result<Vec<u8>> {
self.inner.read_page(page_id)
}
fn sync(&mut self) -> Result<()> {
let mut cfg = self.config.lock().unwrap();
let limit = cfg.fail_sync_after;
FaultConfig::check_and_increment(&mut cfg.sync_count, limit)?;
drop(cfg);
self.inner.sync()
}
fn page_count(&self) -> Result<u64> {
self.inner.page_count()
}
fn close(&mut self) -> Result<()> {
let mut cfg = self.config.lock().unwrap();
let limit = cfg.fail_close_after;
FaultConfig::check_and_increment(&mut cfg.close_count, limit)?;
drop(cfg);
self.inner.close()
}
fn backend_name(&self) -> &'static str {
"fault-injecting"
}
fn is_new(&self) -> bool {
self.inner.is_new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::PAGE_SIZE;
use crate::storage::backend::MemoryBackend;
fn make_page() -> Vec<u8> {
vec![0xAB; PAGE_SIZE]
}
#[test]
fn fault_injecting_backend_fails_on_nth_write() {
let (mut backend, config) = FaultInjectingBackend::with_config(MemoryBackend::new());
assert!(
backend.write_page(0, &make_page()).is_ok(),
"first write should succeed"
);
config.lock().unwrap().fail_write_after = Some(1);
let result = backend.write_page(1, &make_page());
assert!(
result.is_err(),
"second write should fail after fault injection"
);
}
#[test]
fn fault_injecting_backend_fails_on_nth_sync() {
let (mut backend, config) = FaultInjectingBackend::with_config(MemoryBackend::new());
assert!(backend.sync().is_ok(), "first sync should succeed");
config.lock().unwrap().fail_sync_after = Some(1);
let result = backend.sync();
assert!(
result.is_err(),
"second sync should fail after fault injection"
);
}
#[test]
fn reads_are_never_faulted() {
let (mut backend, config) = FaultInjectingBackend::with_config(MemoryBackend::new());
backend.write_page(0, &make_page()).unwrap();
config.lock().unwrap().fail_write_after = Some(0);
assert!(
backend.read_page(0).is_ok(),
"reads should never be faulted"
);
}
#[test]
fn config_can_be_updated_mid_scenario() {
let (mut backend, config) = FaultInjectingBackend::with_config(MemoryBackend::new());
for i in 0..3 {
assert!(backend.write_page(i, &make_page()).is_ok());
}
config.lock().unwrap().fail_write_after = Some(3);
assert!(
backend.write_page(3, &make_page()).is_err(),
"4th write should fail"
);
config.lock().unwrap().fail_write_after = None;
assert!(
backend.write_page(4, &make_page()).is_ok(),
"write should succeed after removing fault"
);
}
}