rudb_io/lib.rs
1//! Files, and the interception shim the crash tests drive.
2//!
3//! Rank 1 in the layer rule. See `xtask/layers.toml` and `spec/18-package-layout.md`.
4//!
5//! Everything that touches a file in this project goes through [`Filesystem`] and [`File`]. Not
6//! most things, everything. `spec/16-testing.md` section 16.5 is explicit about why the shim is
7//! scheduled at M0 and not at M6, where the crash tests that use it live: retrofitting an
8//! interception layer into a codebase that has been calling `File::write` directly for two years is
9//! a much larger job than building against it from the start. So the shim goes in before there is
10//! anything to intercept, and the rule that nothing bypasses it is cheap to keep now and expensive
11//! to establish later.
12//!
13//! # What is here
14//!
15//! [`RealFilesystem`], which is `std::fs` and positional reads and writes.
16//!
17//! [`SimFilesystem`], which is memory, and which records every operation, can be told to fail at a
18//! chosen point, and models the thing that actually happens on a crash: writes that were not
19//! separated by an `fsync` can land in any combination.
20//!
21//! [`Request`] and [`Completion`], which are how a caller states every read it wants in one call
22//! instead of one at a time. `spec/engine/05-scan.md` section 5.3 has the argument and
23//! [`submit`] has the details.
24//!
25//! [`Pool`], which is the threads that serve those requests. They are not the execution threads,
26//! which is the whole idea: a thread blocked on a read is not a core lost to execution, because the
27//! thread that blocked was never an execution thread.
28//!
29//! # What is not here yet
30//!
31//! Direct I/O, io_uring and object storage. `spec/05-storage.md` sections on I/O say the layer ends
32//! up with two backends chosen by measurement at startup, and choosing needs a buffer manager to
33//! generate the depth and a workload to measure. What matters now is that the interface they will
34//! implement exists and that nothing is written against `std::fs` directly in the meantime.
35//!
36//! # Why the methods take `&self`
37//!
38//! Positional I/O does not need exclusive access and the buffer manager is going to want many
39//! readers at once. `read_at` and `write_at` are the whole interface for a reason: a seek plus a
40//! read is two operations with shared state between them, and shared mutable state in the I/O layer
41//! is how a database gets a bug that only appears at sixteen threads.
42
43#![deny(unsafe_code)]
44
45pub mod glob;
46pub mod machine;
47pub mod pool;
48pub mod real;
49pub mod sim;
50pub mod submit;
51
52#[cfg(test)]
53mod scratch;
54
55use std::fmt::Debug;
56use std::path::{Path, PathBuf};
57
58use rudb_common::Result;
59
60pub use glob::expand;
61pub use machine::{default_memory_limit, physical_memory};
62pub use pool::{Config, Pool, Pooled, Stats};
63pub use real::RealFilesystem;
64pub use sim::{Completions, Crash, Op, SimFilesystem};
65pub use submit::{Completion, Filler, Request, Response};
66
67/// How a file is opened.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum OpenMode {
70 /// Must exist. Reads only, and a write is an error.
71 Read,
72 /// Must exist. Reads and writes.
73 ReadWrite,
74 /// Created if it does not exist, opened if it does.
75 Create,
76 /// Created, and an error if it already exists.
77 ///
78 /// The one that matters for a database file, because "create the database" and "open the
79 /// database that is already there" are different intentions and collapsing them is how a
80 /// process ends up writing a header over somebody's data.
81 CreateNew,
82}
83
84impl OpenMode {
85 /// Whether a write through a handle opened this way is allowed.
86 #[must_use]
87 pub fn writable(self) -> bool {
88 !matches!(self, Self::Read)
89 }
90}
91
92/// An open file, addressed by offset rather than by a cursor.
93///
94/// Implementors are shared across threads, which is why every method takes `&self`. A `File` here
95/// is closer to a block device with a name than to `std::fs::File`.
96pub trait File: Debug + Send + Sync {
97 /// Reads into `buf` starting at `offset` and returns how many bytes were read.
98 ///
99 /// A short read at the end of the file is not an error, it is a short read. The caller knows
100 /// how long the file is and what it expected.
101 ///
102 /// # Errors
103 ///
104 /// If the underlying read fails.
105 fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize>;
106
107 /// States every read the caller wants and hands back something to wait on or poll.
108 ///
109 /// This is the interface a scan is written against, per `spec/engine/05-scan.md` section 5.3.
110 /// A row group scan knows all of its byte ranges before it reads any of them, so it says all of
111 /// them at once, and a backend that hears all of them at once can issue them concurrently and
112 /// can coalesce the adjacent ones. Neither is available to a caller that asks one range at a
113 /// time, which is the whole reason the method exists.
114 ///
115 /// The default here is the loop over [`Self::read_at`], so every backend has a correct
116 /// implementation from the moment it exists and a backend with a real queue underneath it
117 /// overrides this rather than being the only thing that works. A failed read fails that one
118 /// request and leaves the rest of the batch alone, because the caller may well be able to
119 /// answer the query from what did arrive, and in any case it is the caller that knows.
120 fn submit(&self, requests: Vec<Request>) -> Completion {
121 let (completion, filler) = Completion::pending(requests.len());
122 for (index, request) in requests.into_iter().enumerate() {
123 let offset = request.offset();
124 let mut buf = request.into_buffer();
125 let outcome =
126 self.read_at(offset, &mut buf).map(|read| Response::new(index, offset, read, buf));
127 filler.finish(index, outcome);
128 }
129 completion
130 }
131
132 /// Reads exactly `buf.len()` bytes starting at `offset`.
133 ///
134 /// # Errors
135 ///
136 /// If the read fails, or if the file ends first. The second case is a real error here, unlike
137 /// in [`Self::read_at`], because a caller who asked for an exact read said it knew the length.
138 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
139 let read = self.read_at(offset, buf)?;
140 if read == buf.len() {
141 Ok(())
142 } else {
143 Err(rudb_common::Error::io(format!(
144 "wanted {} bytes at offset {offset} and the file had {read}",
145 buf.len()
146 )))
147 }
148 }
149
150 /// Writes all of `data` starting at `offset`, extending the file if it has to.
151 ///
152 /// This does not make the write durable. Nothing is durable until [`Self::sync`] returns, and
153 /// a write that has not been synced can be present, absent or reordered against another
154 /// unsynced write after a crash. That is not a quirk of the simulation, it is what the
155 /// hardware does, and it is the reason the simulation models it.
156 ///
157 /// # Errors
158 ///
159 /// If the underlying write fails, or if the file was not opened for writing.
160 fn write_at(&self, offset: u64, data: &[u8]) -> Result<()>;
161
162 /// Makes every write issued before this call durable.
163 ///
164 /// # Errors
165 ///
166 /// If the underlying sync fails. An error here is not recoverable by retrying, per the write
167 /// handling discussion in `spec/11-transactions.md`: a failed `fsync` on Linux can drop the
168 /// dirty pages, so a second call may return success while the data is gone.
169 fn sync(&self) -> Result<()>;
170
171 /// Cuts the file to `len` bytes, or extends it with zeroes.
172 ///
173 /// # Errors
174 ///
175 /// If the underlying truncate fails.
176 fn truncate(&self, len: u64) -> Result<()>;
177
178 /// How many bytes long the file currently is.
179 ///
180 /// # Errors
181 ///
182 /// If the length cannot be determined.
183 fn len(&self) -> Result<u64>;
184
185 /// Whether the file has no bytes in it.
186 ///
187 /// # Errors
188 ///
189 /// If the length cannot be determined.
190 fn is_empty(&self) -> Result<bool> {
191 Ok(self.len()? == 0)
192 }
193}
194
195/// A place files live.
196///
197/// Object stores will implement this too, which is why there is no method that assumes a mutable
198/// hierarchy beyond what a database actually needs. Rename is here because the atomic rename is how
199/// a file gets replaced without a window where it is neither, and because an object store that
200/// cannot do it needs to say so rather than have callers assume.
201pub trait Filesystem: Debug + Send + Sync {
202 /// Opens a file.
203 ///
204 /// # Errors
205 ///
206 /// If the file cannot be opened in the requested mode.
207 fn open(&self, path: &Path, mode: OpenMode) -> Result<Box<dyn File>>;
208
209 /// Whether a path exists.
210 fn exists(&self, path: &Path) -> bool;
211
212 /// Whether a path is a directory.
213 ///
214 /// Separate from [`Filesystem::exists`] because a pattern walk has to tell the two apart:
215 /// `data/*` matches a directory and a file alike and only one of them can be read as a table.
216 fn is_dir(&self, path: &Path) -> bool;
217
218 /// What is directly inside a directory, as whole paths rather than as names.
219 ///
220 /// The order is whatever the filesystem gives, which is not an order. Anything that shows a
221 /// caller more than one of these sorts them, because a directory's own layout differs between
222 /// two machines holding the same files.
223 ///
224 /// # Errors
225 ///
226 /// If the directory cannot be read. A path that is not a directory is the caller's mistake and
227 /// is an error here rather than an empty list.
228 fn read_dir(&self, path: &Path) -> Result<Vec<PathBuf>>;
229
230 /// Deletes a file.
231 ///
232 /// # Errors
233 ///
234 /// If the file cannot be deleted.
235 fn remove(&self, path: &Path) -> Result<()>;
236
237 /// Moves a file, replacing the destination if it exists.
238 ///
239 /// # Errors
240 ///
241 /// If the rename fails.
242 fn rename(&self, from: &Path, to: &Path) -> Result<()>;
243
244 /// Creates a directory and any missing parents.
245 ///
246 /// # Errors
247 ///
248 /// If the directory cannot be created.
249 fn create_dir_all(&self, path: &Path) -> Result<()>;
250
251 /// Makes a directory entry durable, which is what a rename needs before it counts.
252 ///
253 /// Easy to forget and it is the difference between a crash-safe atomic replace and one that
254 /// works on every test and fails on a power cut. The rename itself being atomic says nothing
255 /// about the directory entry having reached the disk.
256 ///
257 /// # Errors
258 ///
259 /// If the directory cannot be synced.
260 fn sync_dir(&self, path: &Path) -> Result<()>;
261}