use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use rudb_common::{Error, Result};
use crate::{File, Filesystem, OpenMode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Op {
Open {
path: PathBuf,
mode: OpenMode,
},
Write {
path: PathBuf,
offset: u64,
len: usize,
},
Sync {
path: PathBuf,
},
Truncate {
path: PathBuf,
len: u64,
},
Rename {
from: PathBuf,
to: PathBuf,
},
Remove {
path: PathBuf,
},
CreateDir {
path: PathBuf,
},
SyncDir {
path: PathBuf,
},
}
impl Op {
#[must_use]
pub fn is_durability_point(&self) -> bool {
matches!(self, Self::Sync { .. } | Self::SyncDir { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Crash {
LosingUnsynced,
KeepingEverything,
Keeping(Vec<u64>),
}
impl Crash {
fn keeps(&self, seq: u64) -> bool {
match self {
Self::LosingUnsynced => false,
Self::KeepingEverything => true,
Self::Keeping(kept) => kept.contains(&seq),
}
}
}
#[derive(Debug, Clone)]
struct Pending {
seq: u64,
change: Change,
}
#[derive(Debug, Clone)]
enum Change {
Write { offset: u64, data: Vec<u8> },
Truncate { len: u64 },
}
#[derive(Debug, Clone, Default)]
struct SimFile {
durable: Vec<u8>,
pending: Vec<Pending>,
}
impl SimFile {
fn visible(&self) -> Vec<u8> {
let mut bytes = self.durable.clone();
for entry in &self.pending {
apply(&mut bytes, &entry.change);
}
bytes
}
}
fn apply(bytes: &mut Vec<u8>, change: &Change) {
match change {
Change::Write { offset, data } => {
let end = *offset as usize + data.len();
if bytes.len() < end {
bytes.resize(end, 0);
}
bytes[*offset as usize..end].copy_from_slice(data);
}
Change::Truncate { len } => bytes.resize(*len as usize, 0),
}
}
#[derive(Debug, Default)]
struct Inner {
files: BTreeMap<PathBuf, SimFile>,
dirs: BTreeSet<PathBuf>,
log: Vec<Op>,
next_seq: u64,
fail_at: Option<usize>,
}
impl Inner {
fn record(&mut self, op: Op) -> Result<()> {
let index = self.log.len();
self.log.push(op);
if self.fail_at == Some(index) {
self.fail_at = None;
return Err(Error::io(format!("injected failure at operation {index}")));
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct SimFilesystem {
inner: Arc<Mutex<Inner>>,
}
impl SimFilesystem {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[must_use]
pub fn ops(&self) -> Vec<Op> {
self.lock().log.clone()
}
#[must_use]
pub fn op_count(&self) -> usize {
self.lock().log.len()
}
pub fn clear_log(&self) {
self.lock().log.clear();
}
pub fn fail_at(&self, index: usize) {
self.lock().fail_at = Some(index);
}
pub fn clear_failure(&self) {
self.lock().fail_at = None;
}
#[must_use]
pub fn pending(&self) -> Vec<(u64, PathBuf)> {
let inner = self.lock();
let mut out: Vec<(u64, PathBuf)> = inner
.files
.iter()
.flat_map(|(path, file)| file.pending.iter().map(|p| (p.seq, path.clone())))
.collect();
out.sort_by_key(|(seq, _)| *seq);
out
}
#[must_use]
pub fn crash(&self, crash: &Crash) -> Self {
let inner = self.lock();
let mut files = BTreeMap::new();
for (path, file) in &inner.files {
let mut bytes = file.durable.clone();
for entry in &file.pending {
if crash.keeps(entry.seq) {
apply(&mut bytes, &entry.change);
}
}
files.insert(path.clone(), SimFile { durable: bytes, pending: Vec::new() });
}
Self {
inner: Arc::new(Mutex::new(Inner {
files,
dirs: inner.dirs.clone(),
log: Vec::new(),
next_seq: 0,
fail_at: None,
})),
}
}
#[must_use]
pub fn durable_contents(&self, path: &Path) -> Option<Vec<u8>> {
self.lock().files.get(path).map(|file| file.durable.clone())
}
#[must_use]
pub fn contents(&self, path: &Path) -> Option<Vec<u8>> {
self.lock().files.get(path).map(SimFile::visible)
}
}
impl Filesystem for SimFilesystem {
fn open(&self, path: &Path, mode: OpenMode) -> Result<Box<dyn File>> {
let mut inner = self.lock();
let exists = inner.files.contains_key(path);
match mode {
OpenMode::Read | OpenMode::ReadWrite if !exists => {
inner.record(Op::Open { path: path.to_path_buf(), mode })?;
return Err(Error::io(format!("{} does not exist", path.display())));
}
OpenMode::CreateNew if exists => {
inner.record(Op::Open { path: path.to_path_buf(), mode })?;
return Err(Error::io(format!("{} already exists", path.display())));
}
_ => {}
}
inner.record(Op::Open { path: path.to_path_buf(), mode })?;
inner.files.entry(path.to_path_buf()).or_default();
Ok(Box::new(SimHandle {
fs: self.clone(),
path: path.to_path_buf(),
writable: mode.writable(),
}))
}
fn exists(&self, path: &Path) -> bool {
let inner = self.lock();
inner.files.contains_key(path) || inner.dirs.contains(path)
}
fn remove(&self, path: &Path) -> Result<()> {
let mut inner = self.lock();
inner.record(Op::Remove { path: path.to_path_buf() })?;
if inner.files.remove(path).is_none() {
return Err(Error::io(format!("{} does not exist", path.display())));
}
Ok(())
}
fn rename(&self, from: &Path, to: &Path) -> Result<()> {
let mut inner = self.lock();
inner.record(Op::Rename { from: from.to_path_buf(), to: to.to_path_buf() })?;
let Some(file) = inner.files.remove(from) else {
return Err(Error::io(format!("{} does not exist", from.display())));
};
inner.files.insert(to.to_path_buf(), file);
Ok(())
}
fn create_dir_all(&self, path: &Path) -> Result<()> {
let mut inner = self.lock();
inner.record(Op::CreateDir { path: path.to_path_buf() })?;
let mut current = PathBuf::new();
for part in path {
current.push(part);
inner.dirs.insert(current.clone());
}
Ok(())
}
fn sync_dir(&self, path: &Path) -> Result<()> {
let mut inner = self.lock();
inner.record(Op::SyncDir { path: path.to_path_buf() })
}
}
#[derive(Debug)]
struct SimHandle {
fs: SimFilesystem,
path: PathBuf,
writable: bool,
}
impl SimHandle {
fn missing(&self) -> Error {
Error::io(format!("{} was removed while open", self.path.display()))
}
}
impl File for SimHandle {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
let inner = self.fs.lock();
let file = inner.files.get(&self.path).ok_or_else(|| self.missing())?;
let bytes = file.visible();
let start = offset as usize;
if start >= bytes.len() {
return Ok(0);
}
let n = buf.len().min(bytes.len() - start);
buf[..n].copy_from_slice(&bytes[start..start + n]);
Ok(n)
}
fn write_at(&self, offset: u64, data: &[u8]) -> Result<()> {
if !self.writable {
return Err(Error::io("this file was opened for reading"));
}
let mut inner = self.fs.lock();
inner.record(Op::Write { path: self.path.clone(), offset, len: data.len() })?;
let seq = inner.next_seq;
inner.next_seq += 1;
let file = inner.files.get_mut(&self.path).ok_or_else(|| self.missing())?;
file.pending.push(Pending { seq, change: Change::Write { offset, data: data.to_vec() } });
Ok(())
}
fn sync(&self) -> Result<()> {
let mut inner = self.fs.lock();
inner.record(Op::Sync { path: self.path.clone() })?;
let file = inner.files.get_mut(&self.path).ok_or_else(|| self.missing())?;
let pending = std::mem::take(&mut file.pending);
let mut durable = std::mem::take(&mut file.durable);
for entry in &pending {
apply(&mut durable, &entry.change);
}
file.durable = durable;
Ok(())
}
fn truncate(&self, len: u64) -> Result<()> {
if !self.writable {
return Err(Error::io("this file was opened for reading"));
}
let mut inner = self.fs.lock();
inner.record(Op::Truncate { path: self.path.clone(), len })?;
let seq = inner.next_seq;
inner.next_seq += 1;
let file = inner.files.get_mut(&self.path).ok_or_else(|| self.missing())?;
file.pending.push(Pending { seq, change: Change::Truncate { len } });
Ok(())
}
fn len(&self) -> Result<u64> {
let inner = self.fs.lock();
let file = inner.files.get(&self.path).ok_or_else(|| self.missing())?;
Ok(file.visible().len() as u64)
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::{Crash, Op, SimFilesystem};
use crate::{Filesystem, OpenMode};
fn write_two_unsynced(fs: &SimFilesystem) {
let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
file.write_at(0, b"AAAA").unwrap();
file.sync().unwrap();
file.write_at(0, b"BBBB").unwrap();
file.write_at(4, b"CCCC").unwrap();
}
#[test]
fn a_reader_sees_a_write_before_it_is_durable() {
let fs = SimFilesystem::new();
let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
file.write_at(0, b"hello").unwrap();
let mut buf = [0u8; 5];
file.read_exact_at(0, &mut buf).unwrap();
assert_eq!(&buf, b"hello");
assert_eq!(fs.durable_contents(Path::new("/db")).unwrap(), Vec::<u8>::new());
}
#[test]
fn a_crash_can_leave_either_both_or_neither() {
let mut seen = Vec::new();
for kept in [vec![], vec![1], vec![2], vec![1, 2]] {
let fs = SimFilesystem::new();
write_two_unsynced(&fs);
let pending = fs.pending();
assert_eq!(pending.len(), 2, "both writes are unsynced");
let after = fs.crash(&Crash::Keeping(kept.clone()));
seen.push(after.durable_contents(Path::new("/db")).unwrap());
}
assert_eq!(seen[0], b"AAAA".to_vec(), "neither landed");
assert_eq!(seen[1], b"BBBB".to_vec(), "the first landed");
assert_eq!(seen[2], b"AAAACCCC".to_vec(), "the second landed and left a hole of zeroes");
assert_eq!(seen[3], b"BBBBCCCC".to_vec(), "both landed");
}
#[test]
fn everything_before_a_sync_survives_a_crash() {
let fs = SimFilesystem::new();
write_two_unsynced(&fs);
let after = fs.crash(&Crash::LosingUnsynced);
assert_eq!(after.durable_contents(Path::new("/db")).unwrap(), b"AAAA".to_vec());
assert!(after.pending().is_empty(), "a crashed filesystem has nothing in flight");
assert_eq!(after.op_count(), 0, "the log belonged to the process that died");
}
#[test]
fn keeping_everything_is_what_a_clean_shutdown_looks_like() {
let fs = SimFilesystem::new();
write_two_unsynced(&fs);
let after = fs.crash(&Crash::KeepingEverything);
assert_eq!(after.durable_contents(Path::new("/db")).unwrap(), b"BBBBCCCC".to_vec());
}
#[test]
fn every_operation_is_recorded_in_order() {
let fs = SimFilesystem::new();
fs.create_dir_all(Path::new("/data")).unwrap();
let file = fs.open(Path::new("/data/db"), OpenMode::CreateNew).unwrap();
file.write_at(0, b"xyz").unwrap();
file.truncate(2).unwrap();
file.sync().unwrap();
fs.rename(Path::new("/data/db"), Path::new("/data/live")).unwrap();
fs.sync_dir(Path::new("/data")).unwrap();
fs.remove(Path::new("/data/live")).unwrap();
let ops = fs.ops();
assert!(matches!(ops[0], Op::CreateDir { .. }));
assert!(matches!(ops[1], Op::Open { .. }));
assert_eq!(ops[2], Op::Write { path: "/data/db".into(), offset: 0, len: 3 });
assert_eq!(ops[3], Op::Truncate { path: "/data/db".into(), len: 2 });
assert!(ops[4].is_durability_point());
assert!(matches!(ops[5], Op::Rename { .. }));
assert!(ops[6].is_durability_point());
assert!(matches!(ops[7], Op::Remove { .. }));
assert_eq!(fs.op_count(), 8);
}
#[test]
fn an_injected_failure_hits_the_operation_it_was_aimed_at() {
let fs = SimFilesystem::new();
let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
fs.fail_at(2);
assert!(file.write_at(0, b"a").is_ok());
assert!(file.write_at(1, b"b").is_err());
assert!(file.write_at(1, b"c").is_ok(), "one shot, not a permanently broken disk");
assert_eq!(fs.contents(Path::new("/db")).unwrap(), b"ac".to_vec());
assert_eq!(fs.op_count(), 4);
}
#[test]
fn a_failed_sync_leaves_the_writes_unsynced() {
let fs = SimFilesystem::new();
let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
file.write_at(0, b"data").unwrap();
fs.fail_at(2);
assert!(file.sync().is_err());
assert_eq!(fs.durable_contents(Path::new("/db")).unwrap(), Vec::<u8>::new());
assert_eq!(fs.pending().len(), 1);
}
#[test]
fn the_enumeration_the_crash_tests_will_run_is_expressible_today() {
let fs = SimFilesystem::new();
let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
file.write_at(0, b"1").unwrap();
file.write_at(1, b"2").unwrap();
file.write_at(2, b"3").unwrap();
let seqs: Vec<u64> = fs.pending().into_iter().map(|(seq, _)| seq).collect();
assert_eq!(seqs.len(), 3);
let mut outcomes = std::collections::BTreeSet::new();
for mask in 0u32..(1 << seqs.len()) {
let kept: Vec<u64> = seqs
.iter()
.enumerate()
.filter(|(bit, _)| mask & (1 << bit) != 0)
.map(|(_, seq)| *seq)
.collect();
let after = fs.crash(&Crash::Keeping(kept));
outcomes.insert(after.durable_contents(Path::new("/db")).unwrap());
}
assert_eq!(outcomes.len(), 8);
}
#[test]
fn a_handle_on_a_removed_file_reports_that_rather_than_pretending() {
let fs = SimFilesystem::new();
let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
file.write_at(0, b"x").unwrap();
fs.remove(Path::new("/db")).unwrap();
assert!(file.len().is_err());
assert!(file.write_at(0, b"y").is_err());
}
#[test]
fn opening_a_file_that_is_not_there_fails_and_creating_one_that_is_fails_too() {
let fs = SimFilesystem::new();
assert!(fs.open(Path::new("/nope"), OpenMode::Read).is_err());
assert!(fs.open(Path::new("/nope"), OpenMode::ReadWrite).is_err());
fs.open(Path::new("/db"), OpenMode::CreateNew).unwrap();
assert!(fs.open(Path::new("/db"), OpenMode::CreateNew).is_err());
assert!(fs.open(Path::new("/db"), OpenMode::Create).is_ok());
}
#[test]
fn clearing_the_log_keeps_the_contents() {
let fs = SimFilesystem::new();
let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
file.write_at(0, b"kept").unwrap();
file.sync().unwrap();
fs.clear_log();
assert_eq!(fs.op_count(), 0);
assert_eq!(fs.durable_contents(Path::new("/db")).unwrap(), b"kept".to_vec());
}
}