1use std::collections::BTreeMap;
12
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15pub enum DiskError {
16 #[error("injected write failure on `{path}`")]
18 WriteFailed {
19 path: String,
21 },
22 #[error("injected fsync failure on `{path}`")]
24 FsyncFailed {
25 path: String,
27 },
28}
29
30#[derive(Debug, Default)]
31struct FileState {
32 durable: Vec<u8>,
33 pending: Vec<u8>,
34}
35
36#[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 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn set_write_failures(&mut self, enabled: bool) {
55 self.fail_writes = enabled;
56 }
57
58 pub fn set_fsync_failures(&mut self, enabled: bool) {
60 self.fail_fsyncs = enabled;
61 }
62
63 pub fn fail_next_write(&mut self) {
65 self.fail_next_writes += 1;
66 }
67
68 pub fn fail_next_fsync(&mut self) {
70 self.fail_next_fsyncs += 1;
71 }
72
73 pub fn set_torn_write_limit(&mut self, limit: Option<usize>) {
76 self.torn_limit = limit;
77 }
78
79 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 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 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 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 pub fn durable_len(&self, path: &str) -> usize {
142 self.files.get(path).map_or(0, |file| file.durable.len())
143 }
144
145 pub fn pending_len(&self, path: &str) -> usize {
147 self.files.get(path).map_or(0, |file| file.pending.len())
148 }
149
150 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}