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}