Skip to main content

rudb_io/
sim.rs

1//! The interception shim.
2//!
3//! `spec/16-testing.md` section 16.5 specifies this: every write, fsync, rename and truncate goes
4//! through a shim that records it and can be told to fail at a chosen point, and to reorder writes
5//! that were not separated by an fsync. The crash consistency tests that drive it are M6 work. The
6//! shim is M0 work because retrofitting it into a codebase that has spent two years calling
7//! `File::write` is a much larger job than building against it from the start.
8//!
9//! # The durability model
10//!
11//! A write goes into a pending list. A read sees it immediately, because that is what the process
12//! sees: the page cache serves the read whether or not the data has reached the disk. A [`sync`]
13//! moves everything pending for that file into the durable image.
14//!
15//! [`SimFilesystem::crash`] then produces a new filesystem holding the durable image plus whichever
16//! subset of the pending writes the caller says survived. That subset is the whole point. A crash
17//! after two writes with no fsync between them can leave either, both or neither, and section 16.5
18//! is specific that testing only the truncation case misses the bugs that actually happen on real
19//! hardware. The subsets are enumerated by the test, exhaustively, which is what lets a crash test
20//! prove something rather than merely fail to find a bug.
21//!
22//! # What this does not model yet
23//!
24//! Directory entry durability. A rename here takes effect immediately, where on a real filesystem
25//! the rename is atomic but the directory entry reaching the disk is a separate question that
26//! [`Filesystem::sync_dir`] answers. Modelling that means a pending list per directory as well as
27//! per file, and it is the difference between testing the atomic replace pattern properly and
28//! testing most of it. It is tracked as issue #19 rather than left as a surprise, and it is needed
29//! before the M6 crash tests can claim to cover the header swap in `spec/05-storage.md`.
30//!
31//! Torn writes within a single `write_at` are not modelled either. A 4 KiB write is atomic on
32//! essentially every device this will run on, and a larger one is decomposed by the caller into
33//! block sized writes, so the interesting reordering is between writes rather than inside one.
34//!
35//! # The read side
36//!
37//! Writes were the whole story while the only thing above this layer was a writer. A reader arrived
38//! at M2d and it has its own three failures, listed in the test gate on the sub-milestone issue:
39//! short reads, reordered completions and an error part way through a batch. All three are
40//! injectable here, through [`SimFilesystem::short_read_at`], [`SimFilesystem::fail_read_at`] and
41//! [`SimFilesystem::complete`], and all three are deterministic, which is the point of doing it
42//! here rather than by unplugging a disk.
43//!
44//! Reads are not in the operation log and do not move the failure point indices, so a crash test
45//! written before any of this still enumerates the same points. They are counted separately, by
46//! [`SimFilesystem::reads_served`], and the read faults are addressed by that counter.
47//!
48//! [`sync`]: crate::File::sync
49
50use std::collections::{BTreeMap, BTreeSet};
51use std::path::{Path, PathBuf};
52use std::sync::{Arc, Mutex};
53
54use rudb_common::{Error, Result};
55
56use crate::submit::{Completion, Request, Response};
57use crate::{File, Filesystem, OpenMode};
58
59/// One recorded operation.
60///
61/// Writes record their length rather than their bytes, because a log of a ClickBench load with the
62/// payloads in it is a log nobody can read and a test nobody can debug.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum Op {
65    /// A file was opened.
66    Open {
67        /// The file.
68        path: PathBuf,
69        /// How it was opened.
70        mode: OpenMode,
71    },
72    /// Bytes were written.
73    Write {
74        /// The file.
75        path: PathBuf,
76        /// Where they went.
77        offset: u64,
78        /// How many.
79        len: usize,
80    },
81    /// A file was made durable.
82    Sync {
83        /// The file.
84        path: PathBuf,
85    },
86    /// A file was cut or extended.
87    Truncate {
88        /// The file.
89        path: PathBuf,
90        /// The new length.
91        len: u64,
92    },
93    /// A file was moved.
94    Rename {
95        /// Where it was.
96        from: PathBuf,
97        /// Where it went.
98        to: PathBuf,
99    },
100    /// A file was deleted.
101    Remove {
102        /// The file.
103        path: PathBuf,
104    },
105    /// A directory was created.
106    CreateDir {
107        /// The directory.
108        path: PathBuf,
109    },
110    /// A directory entry was made durable.
111    SyncDir {
112        /// The directory.
113        path: PathBuf,
114    },
115}
116
117impl Op {
118    /// Whether this operation is one that makes earlier work durable.
119    ///
120    /// The failure point enumeration in section 16.5 cares about these, because the interval
121    /// between two of them is the window in which writes can be reordered against each other.
122    #[must_use]
123    pub fn is_durability_point(&self) -> bool {
124        matches!(self, Self::Sync { .. } | Self::SyncDir { .. })
125    }
126}
127
128/// Which unsynced writes survived a crash.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum Crash {
131    /// None of them. The pessimistic case, and the one most tests reach for first.
132    LosingUnsynced,
133    /// All of them. Not a crash so much as a clean shutdown, and worth testing because a database
134    /// that only works when writes are lost has a different bug.
135    KeepingEverything,
136    /// Exactly these, by the sequence numbers from [`SimFilesystem::pending`].
137    ///
138    /// This is the variant the exhaustive enumeration uses. With three unsynced writes there are
139    /// eight subsets and the test runs all eight.
140    Keeping(Vec<u64>),
141}
142
143impl Crash {
144    fn keeps(&self, seq: u64) -> bool {
145        match self {
146            Self::LosingUnsynced => false,
147            Self::KeepingEverything => true,
148            Self::Keeping(kept) => kept.contains(&seq),
149        }
150    }
151}
152
153/// A write that has been issued and not yet made durable.
154#[derive(Debug, Clone)]
155struct Pending {
156    seq: u64,
157    change: Change,
158}
159
160#[derive(Debug, Clone)]
161enum Change {
162    Write { offset: u64, data: Vec<u8> },
163    Truncate { len: u64 },
164}
165
166#[derive(Debug, Clone, Default)]
167struct SimFile {
168    /// What is on the disk.
169    durable: Vec<u8>,
170    /// What has been written and not synced, in the order it was issued.
171    pending: Vec<Pending>,
172}
173
174impl SimFile {
175    /// What a reader sees, which is the durable image with everything pending applied.
176    fn visible(&self) -> Vec<u8> {
177        let mut bytes = self.durable.clone();
178        for entry in &self.pending {
179            apply(&mut bytes, &entry.change);
180        }
181        bytes
182    }
183}
184
185fn apply(bytes: &mut Vec<u8>, change: &Change) {
186    match change {
187        Change::Write { offset, data } => {
188            let end = *offset as usize + data.len();
189            if bytes.len() < end {
190                bytes.resize(end, 0);
191            }
192            bytes[*offset as usize..end].copy_from_slice(data);
193        }
194        Change::Truncate { len } => bytes.resize(*len as usize, 0),
195    }
196}
197
198/// The order a batch handed to `submit` comes back in.
199///
200/// A reader that only ever works because the answers arrived in the order it asked for them is a
201/// reader that works on a warm page cache and breaks on the first machine where two reads take
202/// different amounts of time. Which is every machine.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
204pub enum Completions {
205    /// In submission order, which is the boring case and the default.
206    #[default]
207    InOrder,
208    /// Last submitted first. Cheap, deterministic, and it catches a caller that reads the first
209    /// response before checking which request it answers.
210    Reversed,
211    /// A deterministic shuffle from this seed, so a failing run is rerunnable from the seed alone.
212    Shuffled(u64),
213}
214
215/// What the simulation has been told to do to the reads.
216#[derive(Debug, Default)]
217struct ReadFaults {
218    /// How many reads have been served since this filesystem was made. The faults below are
219    /// addressed by this number, so a test says which read it wants to break rather than having to
220    /// reach the file handle that will serve it.
221    served: u64,
222    /// Reads that come back with fewer bytes than were asked for, and how many bytes they give.
223    short: BTreeMap<u64, usize>,
224    /// Reads that come back as an error.
225    failing: BTreeSet<u64>,
226    /// The order a submitted batch is completed in.
227    order: Completions,
228}
229
230#[derive(Debug, Default)]
231struct Inner {
232    files: BTreeMap<PathBuf, SimFile>,
233    dirs: BTreeSet<PathBuf>,
234    log: Vec<Op>,
235    next_seq: u64,
236    /// The index in the log at which one operation is made to fail.
237    fail_at: Option<usize>,
238    reads: ReadFaults,
239}
240
241impl Inner {
242    /// Records an operation and says whether the injected failure lands on this one.
243    ///
244    /// The operation is recorded either way. A failure that leaves no trace in the log is a
245    /// failure the test cannot find its way back to.
246    fn record(&mut self, op: Op) -> Result<()> {
247        let index = self.log.len();
248        self.log.push(op);
249        if self.fail_at == Some(index) {
250            self.fail_at = None;
251            return Err(Error::io(format!("injected failure at operation {index}")));
252        }
253        Ok(())
254    }
255
256    /// Serves one read, applying whatever fault was addressed at this read.
257    ///
258    /// The read is counted before anything else can go wrong with it, so an injected failure on
259    /// read seven does not shift what read eight is.
260    fn serve_read(&mut self, path: &Path, offset: u64, buf: &mut [u8]) -> Result<usize> {
261        let number = self.reads.served;
262        self.reads.served += 1;
263        let failing = self.reads.failing.remove(&number);
264        let short = self.reads.short.remove(&number);
265        if failing {
266            return Err(Error::io(format!("injected read failure on read {number}")));
267        }
268        let file = self
269            .files
270            .get(path)
271            .ok_or_else(|| Error::io(format!("{} was removed while open", path.display())))?;
272        let bytes = file.visible();
273        let start = offset as usize;
274        if start >= bytes.len() {
275            return Ok(0);
276        }
277        let mut n = buf.len().min(bytes.len() - start);
278        if let Some(cap) = short {
279            n = n.min(cap);
280        }
281        buf[..n].copy_from_slice(&bytes[start..start + n]);
282        Ok(n)
283    }
284}
285
286/// Permutes a batch of finished reads into the order the simulation says they came back in.
287///
288/// The reads themselves are served in submission order whatever this says, so that the numbering
289/// the read faults are addressed by does not depend on the completion order. Only the order the
290/// caller hears about them in changes, which is the thing being tested.
291fn reorder<T>(outcomes: &mut [T], order: Completions) {
292    match order {
293        Completions::InOrder => {}
294        Completions::Reversed => outcomes.reverse(),
295        Completions::Shuffled(seed) => {
296            // SplitMix64, written out because it is nine lines and the workspace has no
297            // dependencies. Any deterministic generator would do; this one is the one with the
298            // shortest description that passes the tests people run on generators.
299            let mut state = seed;
300            let mut next = move || {
301                state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
302                let mut z = state;
303                z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
304                z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
305                z ^ (z >> 31)
306            };
307            for i in (1..outcomes.len()).rev() {
308                let j = (next() % (i as u64 + 1)) as usize;
309                outcomes.swap(i, j);
310            }
311        }
312    }
313}
314
315/// A filesystem in memory that records what was done to it.
316///
317/// Cloning one gives another handle on the same filesystem, not a copy of it, which is what makes
318/// it usable in the places a real one would be shared.
319#[derive(Debug, Clone, Default)]
320pub struct SimFilesystem {
321    inner: Arc<Mutex<Inner>>,
322}
323
324impl SimFilesystem {
325    /// An empty filesystem.
326    #[must_use]
327    pub fn new() -> Self {
328        Self::default()
329    }
330
331    fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
332        // A poisoned mutex means a test panicked while holding it, and the panic is the finding.
333        // Unwrapping the poison rather than propagating it keeps that panic as the reported
334        // failure instead of burying it under a lock error from an unrelated assertion.
335        self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
336    }
337
338    /// Everything that has been done to this filesystem, in order.
339    #[must_use]
340    pub fn ops(&self) -> Vec<Op> {
341        self.lock().log.clone()
342    }
343
344    /// How many operations have been recorded.
345    ///
346    /// This is the count the failure point enumeration in section 16.5 runs over: record a
347    /// workload, then rerun it once per index with the failure injected there.
348    #[must_use]
349    pub fn op_count(&self) -> usize {
350        self.lock().log.len()
351    }
352
353    /// Forgets the recorded operations, keeping the contents.
354    ///
355    /// For a test that has to set up a database and only wants to enumerate failure points over
356    /// what comes after the setup.
357    pub fn clear_log(&self) {
358        self.lock().log.clear();
359    }
360
361    /// Makes the operation at `index` fail once.
362    ///
363    /// This is error path testing and it is a different mechanism from [`Self::crash`], on purpose.
364    /// An `EIO` on one write is a thing the caller has to handle and keep running from. A crash is
365    /// a thing the caller does not get to handle at all, and the question there is what the next
366    /// process to open the file sees. Conflating them produces a test that proves neither.
367    pub fn fail_at(&self, index: usize) {
368        self.lock().fail_at = Some(index);
369    }
370
371    /// Cancels an injected failure that has not fired.
372    pub fn clear_failure(&self) {
373        self.lock().fail_at = None;
374    }
375
376    /// How many reads have been served since this filesystem was made.
377    ///
378    /// This is the number the read faults below are addressed by, and it is also the byte counting
379    /// hook's coarser sibling: a reader that reads a row group it should have pruned makes more
380    /// reads than one that does not, and the count says so whatever the answer was.
381    #[must_use]
382    pub fn reads_served(&self) -> u64 {
383        self.lock().reads.served
384    }
385
386    /// Makes read number `read` come back with `len` bytes rather than the length asked for.
387    ///
388    /// A short read is not an error, it is a short read, and the reason it is worth injecting is
389    /// that a decoder which treats a returned buffer as full reads whatever was in the buffer
390    /// before, which is a wrong answer and not a crash.
391    pub fn short_read_at(&self, read: u64, len: usize) {
392        self.lock().reads.short.insert(read, len);
393    }
394
395    /// Makes read number `read` fail.
396    ///
397    /// Once, like [`Self::fail_at`], and for the same reason: the interesting question is whether
398    /// the caller survives one failure, not whether it survives a disk that is gone.
399    pub fn fail_read_at(&self, read: u64) {
400        self.lock().reads.failing.insert(read);
401    }
402
403    /// Sets the order a batch handed to `submit` comes back in.
404    pub fn complete(&self, order: Completions) {
405        self.lock().reads.order = order;
406    }
407
408    /// Cancels every injected read fault and puts completions back in submission order.
409    pub fn clear_read_faults(&self) {
410        let mut inner = self.lock();
411        inner.reads.short.clear();
412        inner.reads.failing.clear();
413        inner.reads.order = Completions::InOrder;
414    }
415
416    /// The writes that have been issued and not made durable, as sequence numbers with their file.
417    ///
418    /// These are the numbers [`Crash::Keeping`] takes. The order is the order they were issued in,
419    /// across all files, because two writes to different files race with each other exactly the way
420    /// two writes to one file do.
421    #[must_use]
422    pub fn pending(&self) -> Vec<(u64, PathBuf)> {
423        let inner = self.lock();
424        let mut out: Vec<(u64, PathBuf)> = inner
425            .files
426            .iter()
427            .flat_map(|(path, file)| file.pending.iter().map(|p| (p.seq, path.clone())))
428            .collect();
429        out.sort_by_key(|(seq, _)| *seq);
430        out
431    }
432
433    /// The filesystem a process would find after a crash.
434    ///
435    /// The durable image, plus whichever pending writes `crash` says survived, applied in the order
436    /// they were issued. The result is a fresh filesystem with an empty log, because the log
437    /// belongs to the process that died.
438    #[must_use]
439    pub fn crash(&self, crash: &Crash) -> Self {
440        let inner = self.lock();
441        let mut files = BTreeMap::new();
442        for (path, file) in &inner.files {
443            let mut bytes = file.durable.clone();
444            for entry in &file.pending {
445                if crash.keeps(entry.seq) {
446                    apply(&mut bytes, &entry.change);
447                }
448            }
449            files.insert(path.clone(), SimFile { durable: bytes, pending: Vec::new() });
450        }
451        Self {
452            inner: Arc::new(Mutex::new(Inner {
453                files,
454                dirs: inner.dirs.clone(),
455                log: Vec::new(),
456                next_seq: 0,
457                fail_at: None,
458                reads: ReadFaults::default(),
459            })),
460        }
461    }
462
463    /// The durable contents of a file, ignoring anything unsynced.
464    ///
465    /// For a test that wants to assert what is on the disk without going through a crash first.
466    #[must_use]
467    pub fn durable_contents(&self, path: &Path) -> Option<Vec<u8>> {
468        self.lock().files.get(path).map(|file| file.durable.clone())
469    }
470
471    /// The contents a reader would see right now, unsynced writes included.
472    #[must_use]
473    pub fn contents(&self, path: &Path) -> Option<Vec<u8>> {
474        self.lock().files.get(path).map(SimFile::visible)
475    }
476}
477
478impl Filesystem for SimFilesystem {
479    fn open(&self, path: &Path, mode: OpenMode) -> Result<Box<dyn File>> {
480        let mut inner = self.lock();
481        let exists = inner.files.contains_key(path);
482        match mode {
483            OpenMode::Read | OpenMode::ReadWrite if !exists => {
484                // Recorded before the error, so that a test enumerating failure points sees the
485                // same log whether or not this open succeeded.
486                inner.record(Op::Open { path: path.to_path_buf(), mode })?;
487                return Err(Error::io(format!("{} does not exist", path.display())));
488            }
489            OpenMode::CreateNew if exists => {
490                inner.record(Op::Open { path: path.to_path_buf(), mode })?;
491                return Err(Error::io(format!("{} already exists", path.display())));
492            }
493            _ => {}
494        }
495        inner.record(Op::Open { path: path.to_path_buf(), mode })?;
496        inner.files.entry(path.to_path_buf()).or_default();
497        Ok(Box::new(SimHandle {
498            fs: self.clone(),
499            path: path.to_path_buf(),
500            writable: mode.writable(),
501        }))
502    }
503
504    fn exists(&self, path: &Path) -> bool {
505        let inner = self.lock();
506        inner.files.contains_key(path) || inner.dirs.contains(path)
507    }
508
509    fn is_dir(&self, path: &Path) -> bool {
510        let inner = self.lock();
511        inner.dirs.contains(path)
512    }
513
514    fn read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
515        let inner = self.lock();
516        if !inner.dirs.contains(path) {
517            return Err(Error::io(format!("{} is not a directory", path.display())));
518        }
519        // Not recorded in the operation log. The log is what the crash tests replay and a listing
520        // changes nothing, so an entry for it would be a line every test that lists has to expect.
521        let mut found: Vec<PathBuf> = inner
522            .files
523            .keys()
524            .chain(inner.dirs.iter())
525            .filter(|entry| entry.parent() == Some(path))
526            .cloned()
527            .collect();
528        found.sort();
529        found.dedup();
530        Ok(found)
531    }
532
533    fn remove(&self, path: &Path) -> Result<()> {
534        let mut inner = self.lock();
535        inner.record(Op::Remove { path: path.to_path_buf() })?;
536        if inner.files.remove(path).is_none() {
537            return Err(Error::io(format!("{} does not exist", path.display())));
538        }
539        Ok(())
540    }
541
542    fn rename(&self, from: &Path, to: &Path) -> Result<()> {
543        let mut inner = self.lock();
544        inner.record(Op::Rename { from: from.to_path_buf(), to: to.to_path_buf() })?;
545        let Some(file) = inner.files.remove(from) else {
546            return Err(Error::io(format!("{} does not exist", from.display())));
547        };
548        inner.files.insert(to.to_path_buf(), file);
549        Ok(())
550    }
551
552    fn create_dir_all(&self, path: &Path) -> Result<()> {
553        let mut inner = self.lock();
554        inner.record(Op::CreateDir { path: path.to_path_buf() })?;
555        let mut current = PathBuf::new();
556        for part in path {
557            current.push(part);
558            inner.dirs.insert(current.clone());
559        }
560        Ok(())
561    }
562
563    fn sync_dir(&self, path: &Path) -> Result<()> {
564        let mut inner = self.lock();
565        inner.record(Op::SyncDir { path: path.to_path_buf() })
566    }
567}
568
569/// An open file on a [`SimFilesystem`].
570#[derive(Debug)]
571struct SimHandle {
572    fs: SimFilesystem,
573    path: PathBuf,
574    writable: bool,
575}
576
577impl SimHandle {
578    fn missing(&self) -> Error {
579        Error::io(format!("{} was removed while open", self.path.display()))
580    }
581}
582
583impl File for SimHandle {
584    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
585        self.fs.lock().serve_read(&self.path, offset, buf)
586    }
587
588    fn submit(&self, requests: Vec<Request>) -> Completion {
589        let (completion, filler) = Completion::pending(requests.len());
590        let mut outcomes = Vec::with_capacity(requests.len());
591        for (index, request) in requests.into_iter().enumerate() {
592            let offset = request.offset();
593            let mut buf = request.into_buffer();
594            let outcome =
595                self.read_at(offset, &mut buf).map(|read| Response::new(index, offset, read, buf));
596            outcomes.push((index, outcome));
597        }
598        let order = self.fs.lock().reads.order;
599        reorder(&mut outcomes, order);
600        for (index, outcome) in outcomes {
601            filler.finish(index, outcome);
602        }
603        completion
604    }
605
606    fn write_at(&self, offset: u64, data: &[u8]) -> Result<()> {
607        if !self.writable {
608            return Err(Error::io("this file was opened for reading"));
609        }
610        let mut inner = self.fs.lock();
611        inner.record(Op::Write { path: self.path.clone(), offset, len: data.len() })?;
612        let seq = inner.next_seq;
613        inner.next_seq += 1;
614        let file = inner.files.get_mut(&self.path).ok_or_else(|| self.missing())?;
615        file.pending.push(Pending { seq, change: Change::Write { offset, data: data.to_vec() } });
616        Ok(())
617    }
618
619    fn sync(&self) -> Result<()> {
620        let mut inner = self.fs.lock();
621        inner.record(Op::Sync { path: self.path.clone() })?;
622        let file = inner.files.get_mut(&self.path).ok_or_else(|| self.missing())?;
623        let pending = std::mem::take(&mut file.pending);
624        let mut durable = std::mem::take(&mut file.durable);
625        for entry in &pending {
626            apply(&mut durable, &entry.change);
627        }
628        file.durable = durable;
629        Ok(())
630    }
631
632    fn truncate(&self, len: u64) -> Result<()> {
633        if !self.writable {
634            return Err(Error::io("this file was opened for reading"));
635        }
636        let mut inner = self.fs.lock();
637        inner.record(Op::Truncate { path: self.path.clone(), len })?;
638        let seq = inner.next_seq;
639        inner.next_seq += 1;
640        let file = inner.files.get_mut(&self.path).ok_or_else(|| self.missing())?;
641        file.pending.push(Pending { seq, change: Change::Truncate { len } });
642        Ok(())
643    }
644
645    fn len(&self) -> Result<u64> {
646        let inner = self.fs.lock();
647        let file = inner.files.get(&self.path).ok_or_else(|| self.missing())?;
648        Ok(file.visible().len() as u64)
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use std::path::Path;
655
656    use super::{Completions, Crash, Op, SimFilesystem};
657    use crate::submit::{Request, Response};
658    use crate::{Filesystem, OpenMode};
659
660    fn write_two_unsynced(fs: &SimFilesystem) {
661        let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
662        file.write_at(0, b"AAAA").unwrap();
663        file.sync().unwrap();
664        file.write_at(0, b"BBBB").unwrap();
665        file.write_at(4, b"CCCC").unwrap();
666    }
667
668    #[test]
669    fn a_reader_sees_a_write_before_it_is_durable() {
670        // Because that is what the process sees. The page cache serves the read whether or not the
671        // data reached the disk, and a shim that pretended otherwise would make every test pass
672        // for a reason that has nothing to do with the disk.
673        let fs = SimFilesystem::new();
674        let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
675        file.write_at(0, b"hello").unwrap();
676        let mut buf = [0u8; 5];
677        file.read_exact_at(0, &mut buf).unwrap();
678        assert_eq!(&buf, b"hello");
679        assert_eq!(fs.durable_contents(Path::new("/db")).unwrap(), Vec::<u8>::new());
680    }
681
682    #[test]
683    fn a_crash_can_leave_either_both_or_neither() {
684        // The case section 16.5 is specific about. Two writes with no fsync between them, and all
685        // four outcomes are real. A crash test that only checks the truncation case misses the
686        // bugs that actually happen on hardware.
687        let mut seen = Vec::new();
688        for kept in [vec![], vec![1], vec![2], vec![1, 2]] {
689            let fs = SimFilesystem::new();
690            write_two_unsynced(&fs);
691            let pending = fs.pending();
692            assert_eq!(pending.len(), 2, "both writes are unsynced");
693            let after = fs.crash(&Crash::Keeping(kept.clone()));
694            seen.push(after.durable_contents(Path::new("/db")).unwrap());
695        }
696        assert_eq!(seen[0], b"AAAA".to_vec(), "neither landed");
697        assert_eq!(seen[1], b"BBBB".to_vec(), "the first landed");
698        assert_eq!(seen[2], b"AAAACCCC".to_vec(), "the second landed and left a hole of zeroes");
699        assert_eq!(seen[3], b"BBBBCCCC".to_vec(), "both landed");
700    }
701
702    #[test]
703    fn everything_before_a_sync_survives_a_crash() {
704        let fs = SimFilesystem::new();
705        write_two_unsynced(&fs);
706        let after = fs.crash(&Crash::LosingUnsynced);
707        assert_eq!(after.durable_contents(Path::new("/db")).unwrap(), b"AAAA".to_vec());
708        assert!(after.pending().is_empty(), "a crashed filesystem has nothing in flight");
709        assert_eq!(after.op_count(), 0, "the log belonged to the process that died");
710    }
711
712    #[test]
713    fn keeping_everything_is_what_a_clean_shutdown_looks_like() {
714        let fs = SimFilesystem::new();
715        write_two_unsynced(&fs);
716        let after = fs.crash(&Crash::KeepingEverything);
717        assert_eq!(after.durable_contents(Path::new("/db")).unwrap(), b"BBBBCCCC".to_vec());
718    }
719
720    #[test]
721    fn every_operation_is_recorded_in_order() {
722        let fs = SimFilesystem::new();
723        fs.create_dir_all(Path::new("/data")).unwrap();
724        let file = fs.open(Path::new("/data/db"), OpenMode::CreateNew).unwrap();
725        file.write_at(0, b"xyz").unwrap();
726        file.truncate(2).unwrap();
727        file.sync().unwrap();
728        fs.rename(Path::new("/data/db"), Path::new("/data/live")).unwrap();
729        fs.sync_dir(Path::new("/data")).unwrap();
730        fs.remove(Path::new("/data/live")).unwrap();
731
732        let ops = fs.ops();
733        assert!(matches!(ops[0], Op::CreateDir { .. }));
734        assert!(matches!(ops[1], Op::Open { .. }));
735        assert_eq!(ops[2], Op::Write { path: "/data/db".into(), offset: 0, len: 3 });
736        assert_eq!(ops[3], Op::Truncate { path: "/data/db".into(), len: 2 });
737        assert!(ops[4].is_durability_point());
738        assert!(matches!(ops[5], Op::Rename { .. }));
739        assert!(ops[6].is_durability_point());
740        assert!(matches!(ops[7], Op::Remove { .. }));
741        assert_eq!(fs.op_count(), 8);
742    }
743
744    #[test]
745    fn an_injected_failure_hits_the_operation_it_was_aimed_at() {
746        let fs = SimFilesystem::new();
747        let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
748        // Operation 0 was the open. Aim at operation 2, which is the second write.
749        fs.fail_at(2);
750        assert!(file.write_at(0, b"a").is_ok());
751        assert!(file.write_at(1, b"b").is_err());
752        assert!(file.write_at(1, b"c").is_ok(), "one shot, not a permanently broken disk");
753        // The failed write left no data behind but is in the log, because a test that enumerates
754        // failure points has to be able to find its way back to the point it injected.
755        assert_eq!(fs.contents(Path::new("/db")).unwrap(), b"ac".to_vec());
756        assert_eq!(fs.op_count(), 4);
757    }
758
759    #[test]
760    fn a_failed_sync_leaves_the_writes_unsynced() {
761        // Which is the conservative model. A failed fsync on Linux can drop the dirty pages, so
762        // the one thing a caller must not conclude is that the data is safe.
763        let fs = SimFilesystem::new();
764        let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
765        file.write_at(0, b"data").unwrap();
766        fs.fail_at(2);
767        assert!(file.sync().is_err());
768        assert_eq!(fs.durable_contents(Path::new("/db")).unwrap(), Vec::<u8>::new());
769        assert_eq!(fs.pending().len(), 1);
770    }
771
772    #[test]
773    fn the_enumeration_the_crash_tests_will_run_is_expressible_today() {
774        // Not a crash test, since there is no database to be consistent yet. This is the shape of
775        // the loop from section 16.5, run against the shim itself, so that the apparatus is known
776        // to work before something depends on it being right.
777        let fs = SimFilesystem::new();
778        let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
779        file.write_at(0, b"1").unwrap();
780        file.write_at(1, b"2").unwrap();
781        file.write_at(2, b"3").unwrap();
782        let seqs: Vec<u64> = fs.pending().into_iter().map(|(seq, _)| seq).collect();
783        assert_eq!(seqs.len(), 3);
784
785        let mut outcomes = std::collections::BTreeSet::new();
786        for mask in 0u32..(1 << seqs.len()) {
787            let kept: Vec<u64> = seqs
788                .iter()
789                .enumerate()
790                .filter(|(bit, _)| mask & (1 << bit) != 0)
791                .map(|(_, seq)| *seq)
792                .collect();
793            let after = fs.crash(&Crash::Keeping(kept));
794            outcomes.insert(after.durable_contents(Path::new("/db")).unwrap());
795        }
796        // Eight subsets, eight distinct files, because each write covers a different byte. A
797        // scheme where two subsets produced the same bytes would be one where the enumeration was
798        // doing less work than it looks like.
799        assert_eq!(outcomes.len(), 8);
800    }
801
802    #[test]
803    fn a_handle_on_a_removed_file_reports_that_rather_than_pretending() {
804        let fs = SimFilesystem::new();
805        let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
806        file.write_at(0, b"x").unwrap();
807        fs.remove(Path::new("/db")).unwrap();
808        assert!(file.len().is_err());
809        assert!(file.write_at(0, b"y").is_err());
810    }
811
812    #[test]
813    fn opening_a_file_that_is_not_there_fails_and_creating_one_that_is_fails_too() {
814        let fs = SimFilesystem::new();
815        assert!(fs.open(Path::new("/nope"), OpenMode::Read).is_err());
816        assert!(fs.open(Path::new("/nope"), OpenMode::ReadWrite).is_err());
817        fs.open(Path::new("/db"), OpenMode::CreateNew).unwrap();
818        assert!(fs.open(Path::new("/db"), OpenMode::CreateNew).is_err());
819        assert!(fs.open(Path::new("/db"), OpenMode::Create).is_ok());
820    }
821
822    #[test]
823    fn clearing_the_log_keeps_the_contents() {
824        let fs = SimFilesystem::new();
825        let file = fs.open(Path::new("/db"), OpenMode::Create).unwrap();
826        file.write_at(0, b"kept").unwrap();
827        file.sync().unwrap();
828        fs.clear_log();
829        assert_eq!(fs.op_count(), 0);
830        assert_eq!(fs.durable_contents(Path::new("/db")).unwrap(), b"kept".to_vec());
831    }
832
833    /// Sixteen bytes of `abcdefghijklmnop`, which is short enough to read in an assertion.
834    fn alphabet(fs: &SimFilesystem) -> Box<dyn crate::File> {
835        let file = fs.open(Path::new("/data"), OpenMode::Create).unwrap();
836        file.write_at(0, b"abcdefghijklmnop").unwrap();
837        file.sync().unwrap();
838        file
839    }
840
841    #[test]
842    fn a_submitted_batch_comes_back_whole_and_in_submission_order() {
843        let fs = SimFilesystem::new();
844        let file = alphabet(&fs);
845        let responses = file
846            .submit(vec![Request::new(0, 4), Request::new(8, 4), Request::new(4, 4)])
847            .wait()
848            .unwrap();
849        let bytes: Vec<&[u8]> = responses.iter().map(Response::bytes).collect();
850        assert_eq!(bytes, [b"abcd", b"ijkl", b"efgh"]);
851        assert_eq!(fs.reads_served(), 3);
852    }
853
854    #[test]
855    fn reordered_completions_still_say_which_request_they_answer() {
856        // The failure this is aimed at is a caller that takes the first response to arrive and
857        // assumes it is the first page it asked for. In order the bug is invisible.
858        let fs = SimFilesystem::new();
859        let file = alphabet(&fs);
860        fs.complete(Completions::Reversed);
861        let mut completion = file.submit(vec![Request::new(0, 4), Request::new(4, 4)]);
862        let first = completion.take().unwrap().unwrap();
863        assert_eq!(first.index(), 1);
864        assert_eq!(first.bytes(), b"efgh");
865        assert_eq!(completion.take().unwrap().unwrap().index(), 0);
866        assert!(completion.take().is_none());
867    }
868
869    #[test]
870    fn a_shuffle_is_the_same_shuffle_every_time_for_a_seed() {
871        let order = |seed| {
872            let fs = SimFilesystem::new();
873            let file = alphabet(&fs);
874            fs.complete(Completions::Shuffled(seed));
875            let mut completion =
876                file.submit((0..8).map(|i| Request::new(i * 2, 2)).collect::<Vec<_>>());
877            let mut seen = Vec::new();
878            while let Some(response) = completion.take() {
879                seen.push(response.unwrap().index());
880            }
881            seen
882        };
883        assert_eq!(order(7), order(7), "the same seed is the same run");
884        assert_ne!(order(7), order(8), "and a different one is a different run");
885        let mut sorted = order(7);
886        sorted.sort_unstable();
887        assert_eq!(sorted, (0..8).collect::<Vec<_>>(), "every request is answered exactly once");
888    }
889
890    #[test]
891    fn an_injected_short_read_is_short_and_is_not_an_error() {
892        let fs = SimFilesystem::new();
893        let file = alphabet(&fs);
894        fs.short_read_at(1, 2);
895        let responses = file.submit(vec![Request::new(0, 4), Request::new(4, 4)]).wait().unwrap();
896        assert!(!responses[0].is_short());
897        assert!(responses[1].is_short());
898        assert_eq!(responses[1].bytes(), b"ef");
899        // Once. The next read at the same offset is whole again, because the question is whether
900        // the caller survives one short read rather than whether it survives a broken disk.
901        assert_eq!(file.read_at(4, &mut [0u8; 4]).unwrap(), 4);
902    }
903
904    #[test]
905    fn an_error_part_way_through_a_batch_leaves_the_rest_of_the_batch_alone() {
906        let fs = SimFilesystem::new();
907        let file = alphabet(&fs);
908        fs.fail_read_at(1);
909        let mut completion =
910            file.submit(vec![Request::new(0, 4), Request::new(4, 4), Request::new(8, 4)]);
911        let mut answered = 0;
912        let mut failed = 0;
913        while let Some(outcome) = completion.take() {
914            match outcome {
915                Ok(_) => answered += 1,
916                Err(_) => failed += 1,
917            }
918        }
919        assert_eq!((answered, failed), (2, 1));
920    }
921
922    #[test]
923    fn a_failed_read_does_not_shift_which_read_the_next_fault_lands_on() {
924        let fs = SimFilesystem::new();
925        let file = alphabet(&fs);
926        fs.fail_read_at(0);
927        fs.short_read_at(1, 1);
928        let responses = file.submit(vec![Request::new(0, 4), Request::new(4, 4)]);
929        let mut outcomes = responses;
930        let first = outcomes.take().unwrap();
931        let second = outcomes.take().unwrap();
932        assert!(first.is_err());
933        assert_eq!(second.unwrap().bytes(), b"e");
934    }
935
936    #[test]
937    fn read_at_and_a_batch_of_one_are_the_same_read() {
938        let fs = SimFilesystem::new();
939        let file = alphabet(&fs);
940        let mut buf = [0u8; 5];
941        file.read_exact_at(3, &mut buf).unwrap();
942        let batched = file.submit(vec![Request::new(3, 5)]).wait().unwrap();
943        assert_eq!(batched[0].bytes(), &buf);
944    }
945}