Skip to main content

mongreldb_sim/
disk.rs

1//! Virtual durable store with injectable faults (spec section 9.5,
2//! FND-005).
3//!
4//! Each simulated node owns a [`VirtualDisk`]. Bytes pass through two
5//! stages: `pending` (written, not yet durable) and `durable` (fsynced).
6//! [`VirtualDisk::crash`] models power loss by discarding every pending
7//! byte: crash recovery exposes exactly the fsynced prefix, and unsynced
8//! data is lost. Tests can inject write failures, fsync failures, and
9//! torn (partial) writes.
10
11use std::collections::BTreeMap;
12
13/// The failure surface of a virtual disk operation.
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15pub enum DiskError {
16    /// An injected failure fired during a write.
17    #[error("injected write failure on `{path}`")]
18    WriteFailed {
19        /// The file being written.
20        path: String,
21    },
22    /// An injected failure fired during an fsync.
23    #[error("injected fsync failure on `{path}`")]
24    FsyncFailed {
25        /// The file being synced.
26        path: String,
27    },
28}
29
30#[derive(Debug, Default)]
31struct FileState {
32    durable: Vec<u8>,
33    pending: Vec<u8>,
34}
35
36/// A crashable virtual disk with two-stage durability and fault knobs.
37#[derive(Debug, Default)]
38pub struct VirtualDisk {
39    files: BTreeMap<String, FileState>,
40    fail_writes: bool,
41    fail_fsyncs: bool,
42    fail_next_writes: u32,
43    fail_next_fsyncs: u32,
44    torn_limit: Option<usize>,
45}
46
47impl VirtualDisk {
48    /// An empty disk with no faults armed.
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Fails every write while `enabled` is true.
54    pub fn set_write_failures(&mut self, enabled: bool) {
55        self.fail_writes = enabled;
56    }
57
58    /// Fails every fsync while `enabled` is true.
59    pub fn set_fsync_failures(&mut self, enabled: bool) {
60        self.fail_fsyncs = enabled;
61    }
62
63    /// Fails only the next write.
64    pub fn fail_next_write(&mut self) {
65        self.fail_next_writes += 1;
66    }
67
68    /// Fails only the next fsync.
69    pub fn fail_next_fsync(&mut self) {
70        self.fail_next_fsyncs += 1;
71    }
72
73    /// Truncates every write to at most `limit` bytes (a torn write).
74    /// `None` disables truncation.
75    pub fn set_torn_write_limit(&mut self, limit: Option<usize>) {
76        self.torn_limit = limit;
77    }
78
79    /// Appends bytes to the pending stage. Returns the number of bytes
80    /// accepted, which a torn write may shorten. The bytes are not
81    /// durable until [`VirtualDisk::fsync`].
82    pub fn append(&mut self, path: &str, bytes: &[u8]) -> Result<usize, DiskError> {
83        if self.fail_next_writes > 0 {
84            self.fail_next_writes -= 1;
85            return Err(DiskError::WriteFailed {
86                path: path.to_string(),
87            });
88        }
89        if self.fail_writes {
90            return Err(DiskError::WriteFailed {
91                path: path.to_string(),
92            });
93        }
94        let written = self
95            .torn_limit
96            .map_or(bytes.len(), |limit| bytes.len().min(limit));
97        self.files
98            .entry(path.to_string())
99            .or_default()
100            .pending
101            .extend_from_slice(&bytes[..written]);
102        Ok(written)
103    }
104
105    /// Moves all pending bytes of `path` to the durable stage.
106    pub fn fsync(&mut self, path: &str) -> Result<(), DiskError> {
107        if self.fail_next_fsyncs > 0 {
108            self.fail_next_fsyncs -= 1;
109            return Err(DiskError::FsyncFailed {
110                path: path.to_string(),
111            });
112        }
113        if self.fail_fsyncs {
114            return Err(DiskError::FsyncFailed {
115                path: path.to_string(),
116            });
117        }
118        let file = self.files.entry(path.to_string()).or_default();
119        let pending = std::mem::take(&mut file.pending);
120        file.durable.extend_from_slice(&pending);
121        Ok(())
122    }
123
124    /// Live view: durable bytes followed by not-yet-fsynced bytes.
125    pub fn read(&self, path: &str) -> Vec<u8> {
126        let mut data = self.read_durable(path);
127        if let Some(file) = self.files.get(path) {
128            data.extend_from_slice(&file.pending);
129        }
130        data
131    }
132
133    /// Crash-recovery view: exactly the fsynced prefix.
134    pub fn read_durable(&self, path: &str) -> Vec<u8> {
135        self.files
136            .get(path)
137            .map_or_else(Vec::new, |file| file.durable.clone())
138    }
139
140    /// Number of fsynced bytes for `path`.
141    pub fn durable_len(&self, path: &str) -> usize {
142        self.files.get(path).map_or(0, |file| file.durable.len())
143    }
144
145    /// Number of written-but-not-fsynced bytes for `path`.
146    pub fn pending_len(&self, path: &str) -> usize {
147        self.files.get(path).map_or(0, |file| file.pending.len())
148    }
149
150    /// Simulates power loss: every pending (un-fsynced) byte is lost,
151    /// on every file.
152    pub fn crash(&mut self) {
153        for file in self.files.values_mut() {
154            file.pending.clear();
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn fsync_makes_bytes_durable_and_crash_drops_pending() {
165        let mut disk = VirtualDisk::new();
166        assert_eq!(disk.append("wal", b"aa").unwrap(), 2);
167        assert_eq!(disk.pending_len("wal"), 2);
168        assert_eq!(disk.durable_len("wal"), 0);
169        disk.fsync("wal").unwrap();
170        assert_eq!(disk.append("wal", b"bb").unwrap(), 2);
171        assert_eq!(disk.read("wal"), b"aabb");
172
173        disk.crash();
174        assert_eq!(disk.read("wal"), b"aa");
175        assert_eq!(disk.read_durable("wal"), b"aa");
176        assert_eq!(disk.pending_len("wal"), 0);
177    }
178
179    #[test]
180    fn torn_writes_store_only_a_prefix() {
181        let mut disk = VirtualDisk::new();
182        disk.set_torn_write_limit(Some(3));
183        assert_eq!(disk.append("f", b"abcdef").unwrap(), 3);
184        assert_eq!(disk.read("f"), b"abc");
185
186        disk.set_torn_write_limit(None);
187        assert_eq!(disk.append("f", b"de").unwrap(), 2);
188        assert_eq!(disk.read("f"), b"abcde");
189    }
190
191    #[test]
192    fn injected_write_failures_fire_and_clear() {
193        let mut disk = VirtualDisk::new();
194        disk.fail_next_write();
195        assert_eq!(
196            disk.append("f", b"x"),
197            Err(DiskError::WriteFailed {
198                path: "f".to_string()
199            })
200        );
201        assert_eq!(disk.append("f", b"x").unwrap(), 1);
202
203        disk.set_write_failures(true);
204        assert!(disk.append("f", b"y").is_err());
205        disk.set_write_failures(false);
206        assert_eq!(disk.append("f", b"y").unwrap(), 1);
207        assert_eq!(disk.read("f"), b"xy");
208    }
209
210    #[test]
211    fn injected_fsync_failures_keep_pending_bytes() {
212        let mut disk = VirtualDisk::new();
213        disk.set_fsync_failures(true);
214        disk.append("f", b"abc").unwrap();
215        assert_eq!(
216            disk.fsync("f"),
217            Err(DiskError::FsyncFailed {
218                path: "f".to_string()
219            })
220        );
221        assert_eq!(disk.pending_len("f"), 3);
222        assert_eq!(disk.durable_len("f"), 0);
223
224        disk.set_fsync_failures(false);
225        disk.fail_next_fsync();
226        assert!(disk.fsync("f").is_err());
227        disk.fsync("f").unwrap();
228        assert_eq!(disk.read_durable("f"), b"abc");
229    }
230}