Skip to main content

link_cli/storage/
file_mem.rs

1//! Persistent memory-mapped backing store for `doublets`.
2//!
3//! # Why this wrapper exists
4//!
5//! `doublets` resizes its memory through `RawMem::grow_filled`, whose
6//! default implementation in `platform-mem` fills the **entire** newly
7//! mapped region with `Default::default()` — including the part that is
8//! already backed by bytes on disk:
9//!
10//! ```text
11//! fn grow_filled(&mut self, cap: usize, value: Self::Item) -> Result<&mut [Self::Item]> {
12//!     unsafe { self.grow(cap, |_, (_, uninit)| { uninit::fill(uninit, value); }) }
13//! }
14//! ```
15//!
16//! `FileMapped` computes how many of those elements were already
17//! initialised on disk and passes it as the `inited` argument, but the
18//! default `grow_filled` ignores it. The consequence is that opening an
19//! existing file-mapped `doublets` database zeroes it: every link is
20//! lost. `docs/case-studies/issue-98/evidence/doublets_persistence.rs`
21//! reproduces this against upstream `doublets` directly.
22//!
23//! [`PersistentFileMapped`] fixes this by forwarding to
24//! `RawMem::grow_filled_exact`, which fills only `uninit[inited..]` and
25//! therefore preserves whatever was already written to the file.
26//!
27//! # Durability
28//!
29//! Writes land in a `MAP_SHARED` mapping, which on Linux *is* the page
30//! cache, so they survive a process crash without any explicit action
31//! and are written back by the kernel. `FileMapped` additionally
32//! `sync_all()`s the file when it is dropped, and
33//! [`LinksStorage::flush`](crate::LinksStorage::flush) `fsync`s on
34//! demand for durability across a machine crash.
35
36use std::mem::MaybeUninit;
37use std::path::Path;
38
39use doublets::mem::{FileMapped, RawMem, Result as MemResult};
40
41/// A [`FileMapped`] region that does **not** wipe pre-existing file
42/// contents when `doublets` grows it.
43///
44/// See the module documentation for the upstream behaviour this works
45/// around.
46#[derive(Debug)]
47pub struct PersistentFileMapped<T>(FileMapped<T>);
48
49impl<T> PersistentFileMapped<T> {
50    /// Opens (creating it if needed) the file at `path` and maps it.
51    pub fn from_path<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
52        FileMapped::from_path(path).map(Self)
53    }
54
55    /// Maps an already-opened file.
56    pub fn new(file: std::fs::File) -> std::io::Result<Self> {
57        FileMapped::new(file).map(Self)
58    }
59
60    /// Borrows the wrapped [`FileMapped`].
61    pub fn inner(&self) -> &FileMapped<T> {
62        &self.0
63    }
64}
65
66impl<T> RawMem for PersistentFileMapped<T> {
67    type Item = T;
68
69    fn allocated(&self) -> &[Self::Item] {
70        self.0.allocated()
71    }
72
73    fn allocated_mut(&mut self) -> &mut [Self::Item] {
74        self.0.allocated_mut()
75    }
76
77    unsafe fn grow(
78        &mut self,
79        addition: usize,
80        fill: impl FnOnce(usize, (&mut [Self::Item], &mut [MaybeUninit<Self::Item>])),
81    ) -> MemResult<&mut [Self::Item]> {
82        unsafe { self.0.grow(addition, fill) }
83    }
84
85    fn shrink(&mut self, cap: usize) -> MemResult<()> {
86        self.0.shrink(cap)
87    }
88
89    /// Fills only the genuinely uninitialised tail of the grown region,
90    /// keeping the bytes that were already persisted in the file.
91    fn grow_filled(&mut self, cap: usize, value: Self::Item) -> MemResult<&mut [Self::Item]>
92    where
93        Self::Item: Clone,
94    {
95        // SAFETY: `FileMapped::grow` derives `inited` from the size the
96        // file had before growing, so the elements below it really are
97        // initialised (they were written by a previous session).
98        unsafe { self.0.grow_filled_exact(cap, value) }
99    }
100}