use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DiskError {
#[error("injected write failure on `{path}`")]
WriteFailed {
path: String,
},
#[error("injected fsync failure on `{path}`")]
FsyncFailed {
path: String,
},
}
#[derive(Debug, Default)]
struct FileState {
durable: Vec<u8>,
pending: Vec<u8>,
}
#[derive(Debug, Default)]
pub struct VirtualDisk {
files: BTreeMap<String, FileState>,
fail_writes: bool,
fail_fsyncs: bool,
fail_next_writes: u32,
fail_next_fsyncs: u32,
torn_limit: Option<usize>,
}
impl VirtualDisk {
pub fn new() -> Self {
Self::default()
}
pub fn set_write_failures(&mut self, enabled: bool) {
self.fail_writes = enabled;
}
pub fn set_fsync_failures(&mut self, enabled: bool) {
self.fail_fsyncs = enabled;
}
pub fn fail_next_write(&mut self) {
self.fail_next_writes += 1;
}
pub fn fail_next_fsync(&mut self) {
self.fail_next_fsyncs += 1;
}
pub fn set_torn_write_limit(&mut self, limit: Option<usize>) {
self.torn_limit = limit;
}
pub fn append(&mut self, path: &str, bytes: &[u8]) -> Result<usize, DiskError> {
if self.fail_next_writes > 0 {
self.fail_next_writes -= 1;
return Err(DiskError::WriteFailed {
path: path.to_string(),
});
}
if self.fail_writes {
return Err(DiskError::WriteFailed {
path: path.to_string(),
});
}
let written = self
.torn_limit
.map_or(bytes.len(), |limit| bytes.len().min(limit));
self.files
.entry(path.to_string())
.or_default()
.pending
.extend_from_slice(&bytes[..written]);
Ok(written)
}
pub fn fsync(&mut self, path: &str) -> Result<(), DiskError> {
if self.fail_next_fsyncs > 0 {
self.fail_next_fsyncs -= 1;
return Err(DiskError::FsyncFailed {
path: path.to_string(),
});
}
if self.fail_fsyncs {
return Err(DiskError::FsyncFailed {
path: path.to_string(),
});
}
let file = self.files.entry(path.to_string()).or_default();
let pending = std::mem::take(&mut file.pending);
file.durable.extend_from_slice(&pending);
Ok(())
}
pub fn read(&self, path: &str) -> Vec<u8> {
let mut data = self.read_durable(path);
if let Some(file) = self.files.get(path) {
data.extend_from_slice(&file.pending);
}
data
}
pub fn read_durable(&self, path: &str) -> Vec<u8> {
self.files
.get(path)
.map_or_else(Vec::new, |file| file.durable.clone())
}
pub fn durable_len(&self, path: &str) -> usize {
self.files.get(path).map_or(0, |file| file.durable.len())
}
pub fn pending_len(&self, path: &str) -> usize {
self.files.get(path).map_or(0, |file| file.pending.len())
}
pub fn crash(&mut self) {
for file in self.files.values_mut() {
file.pending.clear();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fsync_makes_bytes_durable_and_crash_drops_pending() {
let mut disk = VirtualDisk::new();
assert_eq!(disk.append("wal", b"aa").unwrap(), 2);
assert_eq!(disk.pending_len("wal"), 2);
assert_eq!(disk.durable_len("wal"), 0);
disk.fsync("wal").unwrap();
assert_eq!(disk.append("wal", b"bb").unwrap(), 2);
assert_eq!(disk.read("wal"), b"aabb");
disk.crash();
assert_eq!(disk.read("wal"), b"aa");
assert_eq!(disk.read_durable("wal"), b"aa");
assert_eq!(disk.pending_len("wal"), 0);
}
#[test]
fn torn_writes_store_only_a_prefix() {
let mut disk = VirtualDisk::new();
disk.set_torn_write_limit(Some(3));
assert_eq!(disk.append("f", b"abcdef").unwrap(), 3);
assert_eq!(disk.read("f"), b"abc");
disk.set_torn_write_limit(None);
assert_eq!(disk.append("f", b"de").unwrap(), 2);
assert_eq!(disk.read("f"), b"abcde");
}
#[test]
fn injected_write_failures_fire_and_clear() {
let mut disk = VirtualDisk::new();
disk.fail_next_write();
assert_eq!(
disk.append("f", b"x"),
Err(DiskError::WriteFailed {
path: "f".to_string()
})
);
assert_eq!(disk.append("f", b"x").unwrap(), 1);
disk.set_write_failures(true);
assert!(disk.append("f", b"y").is_err());
disk.set_write_failures(false);
assert_eq!(disk.append("f", b"y").unwrap(), 1);
assert_eq!(disk.read("f"), b"xy");
}
#[test]
fn injected_fsync_failures_keep_pending_bytes() {
let mut disk = VirtualDisk::new();
disk.set_fsync_failures(true);
disk.append("f", b"abc").unwrap();
assert_eq!(
disk.fsync("f"),
Err(DiskError::FsyncFailed {
path: "f".to_string()
})
);
assert_eq!(disk.pending_len("f"), 3);
assert_eq!(disk.durable_len("f"), 0);
disk.set_fsync_failures(false);
disk.fail_next_fsync();
assert!(disk.fsync("f").is_err());
disk.fsync("f").unwrap();
assert_eq!(disk.read_durable("f"), b"abc");
}
}