Skip to main content

backup/
disk.rs

1//! Everything the worker reads from the store directory, as blocking calls.
2//!
3//! The worker runs each of these on Tokio's blocking pool
4//! ([`tokio::task::spawn_blocking`]), never on a runtime thread: a segment
5//! read can be 64 MiB, and a side file grows with the store's history.
6
7use std::io;
8use std::path::Path;
9use std::time::UNIX_EPOCH;
10
11/// A file's identity as `(len, mtime_nanos)` — the form the engine uses for
12/// `snapshot.bin` ([`mushroomdb::WalPosition::snapshot`]).
13pub type Ident = (u64, u64);
14
15/// [`Ident`] of a file's metadata, computed as the engine computes it.
16pub fn ident_of(meta: &std::fs::Metadata) -> Ident {
17    let mtime_nanos = meta
18        .modified()
19        .ok()
20        .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
21        .map(|d| d.as_nanos() as u64)
22        .unwrap_or(0);
23    (meta.len(), mtime_nanos)
24}
25
26fn not_found_is_none<T>(r: io::Result<T>) -> io::Result<Option<T>> {
27    match r {
28        Ok(v) => Ok(Some(v)),
29        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
30        Err(e) => Err(e),
31    }
32}
33
34/// What one look at the store directory says.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct DiskState {
37    /// `snapshot.bin`'s identity; `None` when there is none.
38    pub snapshot: Option<Ident>,
39    /// `wal.bin`'s length in bytes (0 when absent).
40    pub wal_len: u64,
41    /// Which file `wal.bin` is ([`mushroomdb::WalPosition::wal`]'s form).
42    pub wal: Option<u64>,
43    /// Whether `snapshot.intent` holds anything: a snapshot is running, or
44    /// one stopped half-way.
45    pub intent_nonempty: bool,
46}
47
48pub fn observe(dir: &Path) -> io::Result<DiskState> {
49    let snapshot =
50        not_found_is_none(std::fs::metadata(dir.join("snapshot.bin")))?.map(|m| ident_of(&m));
51    let (wal_len, wal) = wal_len_and_ident(&dir.join("wal.bin"))?.unwrap_or((0, None));
52    let intent_nonempty = not_found_is_none(std::fs::metadata(dir.join("snapshot.intent")))?
53        .is_some_and(|m| m.len() > 0);
54    Ok(DiskState {
55        snapshot,
56        wal_len,
57        wal,
58        intent_nonempty,
59    })
60}
61
62impl DiskState {
63    /// Nothing that pairs a snapshot with a WAL changed between `self` and a
64    /// later look `after`: no snapshot was running at either, the same
65    /// snapshot and the same WAL file are there, and the WAL only grew.
66    pub fn unchanged_until(&self, after: &DiskState) -> bool {
67        !self.intent_nonempty
68            && !after.intent_nonempty
69            && self.snapshot == after.snapshot
70            && self.wal == after.wal
71            && after.wal_len >= self.wal_len
72    }
73}
74
75/// `wal.bin`'s length and identity from one look, or `None` when absent: a
76/// `stat` and the inode on Unix, an open and the file index on Windows,
77/// `None` for the identity elsewhere.
78///
79/// Mirrors `core_storage::fs::len_and_ident`: both must agree on what counts
80/// as the same file so that the worker's view of `wal.bin` matches the
81/// engine's.
82fn wal_len_and_ident(path: &Path) -> io::Result<Option<(u64, Option<u64>)>> {
83    #[cfg(windows)]
84    let look = std::fs::File::open(path).and_then(|f| {
85        let len = f.metadata()?.len();
86        Ok((len, win_file_ident::ident(&f)))
87    });
88    #[cfg(unix)]
89    let look = std::fs::metadata(path).map(|m| {
90        use std::os::unix::fs::MetadataExt as _;
91        (m.len(), Some(m.ino()))
92    });
93    #[cfg(not(any(unix, windows)))]
94    let look = std::fs::metadata(path).map(|m| (m.len(), None));
95    not_found_is_none(look)
96}
97
98/// Windows-only FFI — see the identical copy in `core_storage::fs`.
99#[cfg(windows)]
100mod win_file_ident {
101    use std::fs::File;
102    use std::os::windows::io::AsRawHandle as _;
103
104    #[repr(C)]
105    struct ByHandleFileInformation {
106        dw_file_attributes: u32,
107        ft_creation_time: [u32; 2],
108        ft_last_access_time: [u32; 2],
109        ft_last_write_time: [u32; 2],
110        dw_volume_serial_number: u32,
111        n_file_size_high: u32,
112        n_file_size_low: u32,
113        n_number_of_links: u32,
114        n_file_index_high: u32,
115        n_file_index_low: u32,
116    }
117
118    #[link(name = "kernel32")]
119    extern "system" {
120        fn GetFileInformationByHandle(
121            h_file: *mut std::ffi::c_void,
122            lp_file_information: *mut ByHandleFileInformation,
123        ) -> i32;
124    }
125
126    pub(super) fn ident(f: &File) -> Option<u64> {
127        // SAFETY: `f` is a valid open handle; zeroed struct satisfies the
128        // output-parameter contract; no invariants are violated on failure.
129        let mut info = ByHandleFileInformation {
130            dw_file_attributes: 0,
131            ft_creation_time: [0; 2],
132            ft_last_access_time: [0; 2],
133            ft_last_write_time: [0; 2],
134            dw_volume_serial_number: 0,
135            n_file_size_high: 0,
136            n_file_size_low: 0,
137            n_number_of_links: 0,
138            n_file_index_high: 0,
139            n_file_index_low: 0,
140        };
141        let ok = unsafe { GetFileInformationByHandle(f.as_raw_handle(), &mut info) };
142        if ok != 0 {
143            Some((info.n_file_index_high as u64) << 32 | info.n_file_index_low as u64)
144        } else {
145            None
146        }
147    }
148}
149
150/// `snapshot.bin` opened, with the identity of the file actually opened.
151pub fn open_snapshot(dir: &Path) -> io::Result<Option<(std::fs::File, Ident)>> {
152    let Some(f) = not_found_is_none(std::fs::File::open(dir.join("snapshot.bin")))? else {
153        return Ok(None);
154    };
155    let ident = ident_of(&f.metadata()?);
156    Ok(Some((f, ident)))
157}
158
159/// Every `wal.N.archive` in the directory, as `(n, len)`, sorted by `n`.
160pub fn local_archives(dir: &Path) -> io::Result<Vec<(u64, u64)>> {
161    let mut out = Vec::new();
162    for entry in std::fs::read_dir(dir)? {
163        let entry = entry?;
164        let name = entry.file_name();
165        if let Some(n) = crate::manifest::parse_archive_name(&name.to_string_lossy()) {
166            if let Some(m) = not_found_is_none(entry.metadata())? {
167                out.push((n, m.len()));
168            }
169        }
170    }
171    out.sort_unstable();
172    Ok(out)
173}
174
175/// A side file's identity, and its bytes when the identity differs from
176/// `known`. `Ok(None)` when the file does not exist.
177pub fn read_side(
178    dir: &Path,
179    name: &str,
180    known: Option<Ident>,
181) -> io::Result<Option<(Ident, Option<Vec<u8>>)>> {
182    let path = dir.join(name);
183    let Some(meta) = not_found_is_none(std::fs::metadata(&path))? else {
184        return Ok(None);
185    };
186    let ident = ident_of(&meta);
187    if known == Some(ident) {
188        return Ok(Some((ident, None)));
189    }
190    match not_found_is_none(std::fs::read(&path))? {
191        Some(bytes) => Ok(Some((ident, Some(bytes)))),
192        None => Ok(None),
193    }
194}
195
196/// The store file naming the backup lineage it belongs to.
197pub const LINEAGE_FILE: &str = "backup.lineage";
198
199/// The store's backup lineage, or `None` when it has never been backed up.
200pub fn read_lineage(dir: &Path) -> io::Result<Option<String>> {
201    Ok(
202        not_found_is_none(std::fs::read_to_string(dir.join(LINEAGE_FILE)))?
203            .map(|s| s.trim().to_string())
204            .filter(|s| !s.is_empty()),
205    )
206}
207
208/// Give the store the backup lineage `id`: written to a temporary file,
209/// fsynced, renamed into place, and the directory fsynced. The one file the
210/// backup worker ever writes into a store directory.
211pub fn create_lineage(dir: &Path, id: &str) -> io::Result<()> {
212    use std::io::Write as _;
213    let tmp = dir.join(format!("{LINEAGE_FILE}.tmp"));
214    {
215        let mut f = std::fs::File::create(&tmp)?;
216        f.write_all(id.as_bytes())?;
217        f.sync_all()?;
218    }
219    std::fs::rename(&tmp, dir.join(LINEAGE_FILE))?;
220    #[cfg(unix)]
221    std::fs::File::open(dir)?.sync_all()?;
222    Ok(())
223}
224
225/// The store file a restore-on-boot writes: which lineage and restore point
226/// the store was restored from. Never uploaded (it is not a side file).
227pub const PARENT_FILE: &str = "backup.parent";
228
229/// What [`PARENT_FILE`] holds.
230///
231/// A store restored at boot has a new lineage of its own, like every restore.
232/// This record is what lets its backup continue under the prefix it was
233/// restored from: the worker adopts that prefix only while `LATEST` still
234/// names exactly this manifest of exactly this lineage, and from then on
235/// writes only objects of its own lineage beside the parent's, which it never
236/// overwrites or deletes.
237#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
238pub struct ParentRecord {
239    /// The lineage of the backup the store was restored from.
240    pub lineage: String,
241    /// The key of the manifest it was restored from.
242    pub manifest: String,
243    /// The commits the restored store held.
244    pub commit_seq: u64,
245}
246
247/// The store's parent record, or `None` when it has none (or one that does
248/// not parse: it then adopts nothing).
249pub fn read_parent(dir: &Path) -> io::Result<Option<ParentRecord>> {
250    Ok(not_found_is_none(std::fs::read(dir.join(PARENT_FILE)))?
251        .and_then(|b| serde_json::from_slice(&b).ok()))
252}
253
254/// Write [`PARENT_FILE`] as [`create_lineage`] writes the lineage: a
255/// temporary file, fsynced, renamed into place, the directory fsynced.
256pub fn create_parent(dir: &Path, record: &ParentRecord) -> io::Result<()> {
257    use std::io::Write as _;
258    let bytes = serde_json::to_vec(record).map_err(io::Error::other)?;
259    let tmp = dir.join(format!("{PARENT_FILE}.tmp"));
260    {
261        let mut f = std::fs::File::create(&tmp)?;
262        f.write_all(&bytes)?;
263        f.sync_all()?;
264    }
265    std::fs::rename(&tmp, dir.join(PARENT_FILE))?;
266    #[cfg(unix)]
267    std::fs::File::open(dir)?.sync_all()?;
268    Ok(())
269}
270
271/// CRC-32 of a whole file, read from its start; the file is left at its
272/// start again.
273pub fn crc32_of(file: &mut std::fs::File) -> io::Result<String> {
274    use std::io::{Read as _, Seek as _, SeekFrom};
275    file.seek(SeekFrom::Start(0))?;
276    let mut h = crc32fast::Hasher::new();
277    let mut buf = vec![0u8; 1 << 20];
278    loop {
279        let n = file.read(&mut buf)?;
280        if n == 0 {
281            break;
282        }
283        h.update(&buf[..n]);
284    }
285    file.seek(SeekFrom::Start(0))?;
286    Ok(crate::manifest::format_crc32(h.finalize()))
287}
288
289/// The store's horizon floor (`wal.floor`), or 0.
290pub fn floor(dir: &Path) -> u64 {
291    std::fs::read(dir.join("wal.floor"))
292        .ok()
293        .and_then(|b| {
294            b.get(..8)
295                .map(|s| u64::from_le_bytes(s.try_into().unwrap()))
296        })
297        .unwrap_or(0)
298}