Skip to main content

rudb_io/
real.rs

1//! The filesystem that is an actual filesystem.
2//!
3//! Thin on purpose. Everything interesting about I/O in this project happens either above this,
4//! in the buffer manager, or below it, in the kernel. What is here is the mapping from the
5//! interface in the crate root onto positional reads and writes, and nothing else.
6//!
7//! Direct I/O and io_uring are not here. `spec/05-storage.md` says the layer ends up with two
8//! backends chosen by measurement at startup, because io_uring is a win at queue depth and a loss
9//! at low depth, and the Conviva result is the published case of somebody migrating to it and
10//! getting slower. Choosing between them needs a buffer manager to generate the depth and a
11//! workload to measure, and both arrive at M2.
12
13use std::fs::{File as StdFile, OpenOptions};
14use std::path::{Path, PathBuf};
15
16use rudb_common::{Error, Result};
17
18use crate::{File, Filesystem, OpenMode};
19
20/// Files on the machine this process is running on.
21#[derive(Debug, Default, Clone, Copy)]
22pub struct RealFilesystem;
23
24impl RealFilesystem {
25    /// A handle on the real filesystem.
26    #[must_use]
27    pub fn new() -> Self {
28        Self
29    }
30}
31
32impl Filesystem for RealFilesystem {
33    fn open(&self, path: &Path, mode: OpenMode) -> Result<Box<dyn File>> {
34        let mut options = OpenOptions::new();
35        match mode {
36            OpenMode::Read => {
37                options.read(true);
38            }
39            OpenMode::ReadWrite => {
40                options.read(true).write(true);
41            }
42            OpenMode::Create => {
43                options.read(true).write(true).create(true);
44            }
45            OpenMode::CreateNew => {
46                options.read(true).write(true).create_new(true);
47            }
48        }
49        let file = options
50            .open(path)
51            .map_err(|e| Error::io(format!("could not open {}: {e}", path.display())))?;
52        Ok(Box::new(RealFile { file, writable: mode.writable() }))
53    }
54
55    fn exists(&self, path: &Path) -> bool {
56        path.exists()
57    }
58
59    fn is_dir(&self, path: &Path) -> bool {
60        path.is_dir()
61    }
62
63    fn read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
64        let entries = std::fs::read_dir(path)
65            .map_err(|e| Error::io(format!("could not read {}: {e}", path.display())))?;
66        let mut found = Vec::new();
67        for entry in entries {
68            let entry =
69                entry.map_err(|e| Error::io(format!("could not read {}: {e}", path.display())))?;
70            found.push(entry.path());
71        }
72        Ok(found)
73    }
74
75    fn remove(&self, path: &Path) -> Result<()> {
76        std::fs::remove_file(path)
77            .map_err(|e| Error::io(format!("could not remove {}: {e}", path.display())))
78    }
79
80    fn rename(&self, from: &Path, to: &Path) -> Result<()> {
81        std::fs::rename(from, to).map_err(|e| {
82            Error::io(format!("could not rename {} to {}: {e}", from.display(), to.display()))
83        })
84    }
85
86    fn create_dir_all(&self, path: &Path) -> Result<()> {
87        std::fs::create_dir_all(path)
88            .map_err(|e| Error::io(format!("could not create {}: {e}", path.display())))
89    }
90
91    fn sync_dir(&self, path: &Path) -> Result<()> {
92        // Windows has no notion of opening a directory to sync it, and its rename durability model
93        // is different enough that pretending otherwise would be worse than saying so. The crash
94        // tests run the simulation, which models the durability rules explicitly, so the platform
95        // gap shows up there rather than being papered over here.
96        #[cfg(windows)]
97        {
98            let _ = path;
99            Ok(())
100        }
101        #[cfg(not(windows))]
102        {
103            let dir = StdFile::open(path)
104                .map_err(|e| Error::io(format!("could not open {}: {e}", path.display())))?;
105            dir.sync_all().map_err(|e| Error::io(format!("could not sync {}: {e}", path.display())))
106        }
107    }
108}
109
110/// An open file on the real filesystem.
111#[derive(Debug)]
112struct RealFile {
113    file: StdFile,
114    writable: bool,
115}
116
117impl File for RealFile {
118    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
119        #[cfg(unix)]
120        let result = std::os::unix::fs::FileExt::read_at(&self.file, buf, offset);
121        #[cfg(windows)]
122        let result = std::os::windows::fs::FileExt::seek_read(&self.file, buf, offset);
123        result.map_err(|e| Error::io(format!("read at {offset} failed: {e}")))
124    }
125
126    fn write_at(&self, offset: u64, data: &[u8]) -> Result<()> {
127        if !self.writable {
128            return Err(Error::io("this file was opened for reading"));
129        }
130        let mut written = 0usize;
131        while written < data.len() {
132            let at = offset + written as u64;
133            let chunk = &data[written..];
134            #[cfg(unix)]
135            let result = std::os::unix::fs::FileExt::write_at(&self.file, chunk, at);
136            #[cfg(windows)]
137            let result = std::os::windows::fs::FileExt::seek_write(&self.file, chunk, at);
138            let n = result.map_err(|e| Error::io(format!("write at {at} failed: {e}")))?;
139            if n == 0 {
140                return Err(Error::io(format!("write at {at} wrote nothing")));
141            }
142            written += n;
143        }
144        Ok(())
145    }
146
147    fn sync(&self) -> Result<()> {
148        // `sync_all` and not `sync_data`. The difference is the metadata, and a file whose data is
149        // durable while its length is not is a file that reads back short after a crash.
150        self.file.sync_all().map_err(|e| Error::io(format!("sync failed: {e}")))
151    }
152
153    fn truncate(&self, len: u64) -> Result<()> {
154        if !self.writable {
155            return Err(Error::io("this file was opened for reading"));
156        }
157        self.file.set_len(len).map_err(|e| Error::io(format!("truncate to {len} failed: {e}")))
158    }
159
160    fn len(&self) -> Result<u64> {
161        Ok(self.file.metadata().map_err(|e| Error::io(format!("stat failed: {e}")))?.len())
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::RealFilesystem;
168    use crate::scratch::TempDir;
169    use crate::submit::Request;
170    use crate::{Filesystem, OpenMode};
171
172    #[test]
173    fn a_file_reads_back_what_was_written_at_the_offset_it_was_written_to() {
174        let dir = TempDir::new("roundtrip");
175        let fs = RealFilesystem::new();
176        let path = dir.join("data");
177        let file = fs.open(&path, OpenMode::CreateNew).unwrap();
178        file.write_at(0, b"hello").unwrap();
179        file.write_at(16, b"world").unwrap();
180        file.sync().unwrap();
181
182        let mut buf = [0u8; 5];
183        file.read_exact_at(16, &mut buf).unwrap();
184        assert_eq!(&buf, b"world");
185        assert_eq!(file.len().unwrap(), 21);
186        // The gap between the two writes is zeroes, not garbage, which is what makes a sparse
187        // write safe to do and what a block allocator will rely on.
188        let mut gap = [0xffu8; 11];
189        file.read_exact_at(5, &mut gap).unwrap();
190        assert_eq!(gap, [0u8; 11]);
191    }
192
193    #[test]
194    fn create_new_refuses_to_open_a_file_that_is_already_there() {
195        // "Create the database" and "open the database that is already there" are different
196        // intentions, and collapsing them is how a process writes a header over somebody's data.
197        let dir = TempDir::new("createnew");
198        let fs = RealFilesystem::new();
199        let path = dir.join("db");
200        fs.open(&path, OpenMode::CreateNew).unwrap();
201        assert!(fs.open(&path, OpenMode::CreateNew).is_err());
202        assert!(fs.open(&path, OpenMode::Create).is_ok());
203    }
204
205    #[test]
206    fn a_read_only_handle_refuses_to_write() {
207        let dir = TempDir::new("readonly");
208        let fs = RealFilesystem::new();
209        let path = dir.join("data");
210        fs.open(&path, OpenMode::CreateNew).unwrap().write_at(0, b"x").unwrap();
211        let file = fs.open(&path, OpenMode::Read).unwrap();
212        assert!(file.write_at(0, b"y").is_err());
213        assert!(file.truncate(0).is_err());
214    }
215
216    #[test]
217    fn a_short_read_at_the_end_is_a_short_read_and_an_exact_read_is_an_error() {
218        let dir = TempDir::new("shortread");
219        let fs = RealFilesystem::new();
220        let path = dir.join("data");
221        let file = fs.open(&path, OpenMode::CreateNew).unwrap();
222        file.write_at(0, b"abc").unwrap();
223        let mut buf = [0u8; 8];
224        assert_eq!(file.read_at(0, &mut buf).unwrap(), 3);
225        assert!(file.read_exact_at(0, &mut buf).is_err());
226    }
227
228    #[test]
229    fn truncate_cuts_and_extends() {
230        let dir = TempDir::new("truncate");
231        let fs = RealFilesystem::new();
232        let path = dir.join("data");
233        let file = fs.open(&path, OpenMode::CreateNew).unwrap();
234        file.write_at(0, b"abcdefgh").unwrap();
235        file.truncate(3).unwrap();
236        assert_eq!(file.len().unwrap(), 3);
237        file.truncate(6).unwrap();
238        let mut buf = [0xffu8; 6];
239        file.read_exact_at(0, &mut buf).unwrap();
240        assert_eq!(&buf, b"abc\0\0\0");
241    }
242
243    #[test]
244    fn a_batch_of_reads_comes_back_answering_the_requests_it_was_given() {
245        // The real filesystem takes the default `submit`, which is the loop over `read_at`. That
246        // is still worth a test, because the default is what every backend has until somebody
247        // writes it a queue, and a scan is written against `submit` from its first line.
248        let dir = TempDir::new("submit");
249        let fs = RealFilesystem::new();
250        let path = dir.join("data");
251        let file = fs.open(&path, OpenMode::CreateNew).unwrap();
252        file.write_at(0, b"abcdefghijklmnop").unwrap();
253        file.sync().unwrap();
254
255        let responses = file
256            .submit(vec![Request::new(8, 4), Request::new(0, 4), Request::new(12, 8)])
257            .wait()
258            .unwrap();
259        assert_eq!(responses.len(), 3);
260        assert_eq!(responses[0].bytes(), b"ijkl");
261        assert_eq!(responses[1].bytes(), b"abcd");
262        // The last one runs off the end of the file, which is a short read and not an error.
263        assert!(responses[2].is_short());
264        assert_eq!(responses[2].bytes(), b"mnop");
265    }
266
267    #[test]
268    fn rename_replaces_and_remove_removes() {
269        let dir = TempDir::new("rename");
270        let fs = RealFilesystem::new();
271        let from = dir.join("new");
272        let to = dir.join("live");
273        fs.open(&to, OpenMode::CreateNew).unwrap().write_at(0, b"old").unwrap();
274        fs.open(&from, OpenMode::CreateNew).unwrap().write_at(0, b"new").unwrap();
275        fs.rename(&from, &to).unwrap();
276        assert!(!fs.exists(&from));
277
278        let mut buf = [0u8; 3];
279        fs.open(&to, OpenMode::Read).unwrap().read_exact_at(0, &mut buf).unwrap();
280        assert_eq!(&buf, b"new");
281
282        fs.remove(&to).unwrap();
283        assert!(!fs.exists(&to));
284    }
285}