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//! The bug is reported upstream as
23//! <https://github.com/linksplatform/mem-rs/issues/36>.
24//!
25//! [`PersistentFileMapped`] fixes this by forwarding to
26//! `RawMem::grow_filled_exact`, which fills only `uninit[inited..]` and
27//! therefore preserves whatever was already written to the file.
28//!
29//! # Reopening an existing mapping
30//!
31//! Upstream [`FileMapped::new`] starts with a logical capacity of zero even
32//! when its file already contains initialized elements. Use
33//! [`PersistentFileMapped::open_existing`] when the file's bytes are a complete
34//! persisted mapping and its capacity must be visible immediately. The safe
35//! API is available only for [`FileMappedValue`] types, whose representations
36//! this crate can soundly adopt without asking each caller for an `unsafe`
37//! block.
38//!
39//! # Durability
40//!
41//! Writes land in a `MAP_SHARED` mapping, which on Linux *is* the page
42//! cache, so they survive a process crash without any explicit action
43//! and are written back by the kernel. `FileMapped` additionally
44//! `sync_all()`s the file when it is dropped, and
45//! [`LinksStorage::flush`](crate::LinksStorage::flush) `fsync`s on
46//! demand for durability across a machine crash.
47
48use std::fs::File;
49use std::io;
50use std::mem::{self, MaybeUninit};
51use std::path::Path;
52
53use doublets::data::LinkReference;
54use doublets::mem::{FileMapped, RawMem, Result as MemResult};
55use doublets::unit::LinkPart;
56
57/// A value whose representation can safely be adopted from existing file bytes.
58///
59/// This is the safety boundary used by
60/// [`PersistentFileMapped::open_existing`]. The crate implements it for the
61/// unsigned integer link-address types and for [`LinkPart`] values containing
62/// those addresses.
63///
64/// # Safety
65///
66/// Every initialized byte pattern of `Self` must represent a valid value, it
67/// must be safe to drop any such value, and `Self` must not be zero-sized.
68pub unsafe trait FileMappedValue {}
69
70macro_rules! impl_file_mapped_value_for_unsigned {
71    ($($ty:ty),+ $(,)?) => {
72        $(
73            // SAFETY: every bit pattern is valid for unsigned integers, they
74            // have no drop glue, and none of these types is zero-sized.
75            unsafe impl FileMappedValue for $ty {}
76        )+
77    };
78}
79
80impl_file_mapped_value_for_unsigned!(u8, u16, u32, u64, u128, usize);
81
82// SAFETY: `LinkPart<T>` is `repr(C)` and consists only of eight `T` fields.
83// When every bit pattern is valid for `T`, it is therefore valid for the
84// complete link part as well, and dropping it only drops those fields.
85unsafe impl<T: FileMappedValue + LinkReference> FileMappedValue for LinkPart<T> {}
86
87/// A [`FileMapped`] region that does **not** wipe pre-existing file
88/// contents when `doublets` grows it.
89///
90/// See the module documentation for the upstream behaviour this works
91/// around.
92#[derive(Debug)]
93pub struct PersistentFileMapped<T>(FileMapped<T>);
94
95impl<T> PersistentFileMapped<T> {
96    /// Opens (creating it if needed) the file at `path` and maps it.
97    pub fn from_path<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
98        FileMapped::from_path(path).map(Self)
99    }
100
101    /// Maps an already-opened file.
102    pub fn new(file: File) -> io::Result<Self> {
103        FileMapped::new(file).map(Self)
104    }
105
106    /// Borrows the wrapped [`FileMapped`].
107    pub fn inner(&self) -> &FileMapped<T> {
108        &self.0
109    }
110}
111
112impl<T: FileMappedValue> PersistentFileMapped<T> {
113    /// Maps `file` and adopts the capacity represented by its existing bytes.
114    ///
115    /// Unlike [`Self::new`], which starts with a logical capacity of zero, this
116    /// constructor makes every complete `T` already present in the file
117    /// immediately visible through [`RawMem::allocated`]. Any trailing bytes
118    /// that do not form a complete `T` are left untouched and ignored.
119    ///
120    /// ```no_run
121    /// #![deny(unsafe_code)]
122    /// use std::fs::File;
123    ///
124    /// use link_cli::doublets::unit::LinkPart;
125    /// use link_cli::PersistentFileMapped;
126    ///
127    /// # fn main() -> std::io::Result<()> {
128    /// let file = File::options().read(true).write(true).open("links.data")?;
129    /// let mapped = PersistentFileMapped::<LinkPart<usize>>::open_existing(file)?;
130    /// # let _ = mapped;
131    /// # Ok(())
132    /// # }
133    /// ```
134    pub fn open_existing(file: File) -> io::Result<Self> {
135        // Read this before `FileMapped::new`, which extends sub-page files to
136        // its minimum mapping size. That padding was not part of the persisted
137        // contents and must not become visible as logical items.
138        let byte_len = file.metadata()?.len();
139        let mut mapped = FileMapped::new(file)?;
140        let item_size = mem::size_of::<T>() as u64;
141        debug_assert_ne!(item_size, 0, "FileMappedValue must not be zero-sized");
142        let capacity = usize::try_from(byte_len / item_size).map_err(|_| {
143            io::Error::new(
144                io::ErrorKind::InvalidData,
145                "file capacity does not fit in usize",
146            )
147        })?;
148
149        // SAFETY: `FileMapped::new` guarantees the file contains `byte_len`
150        // initialized bytes, and `FileMappedValue` guarantees every byte
151        // pattern in each complete item is a valid, safely droppable `T`.
152        if capacity != 0 {
153            unsafe { mapped.grow_assumed(capacity) }.map_err(|error| match error {
154                doublets::mem::Error::System(error) => error,
155                error => io::Error::other(error),
156            })?;
157        }
158
159        Ok(Self(mapped))
160    }
161}
162
163impl<T> RawMem for PersistentFileMapped<T> {
164    type Item = T;
165
166    fn allocated(&self) -> &[Self::Item] {
167        self.0.allocated()
168    }
169
170    fn allocated_mut(&mut self) -> &mut [Self::Item] {
171        self.0.allocated_mut()
172    }
173
174    unsafe fn grow(
175        &mut self,
176        addition: usize,
177        fill: impl FnOnce(usize, (&mut [Self::Item], &mut [MaybeUninit<Self::Item>])),
178    ) -> MemResult<&mut [Self::Item]> {
179        unsafe { self.0.grow(addition, fill) }
180    }
181
182    fn shrink(&mut self, cap: usize) -> MemResult<()> {
183        self.0.shrink(cap)
184    }
185
186    /// Fills only the genuinely uninitialised tail of the grown region,
187    /// keeping the bytes that were already persisted in the file.
188    fn grow_filled(&mut self, cap: usize, value: Self::Item) -> MemResult<&mut [Self::Item]>
189    where
190        Self::Item: Clone,
191    {
192        // SAFETY: `FileMapped::grow` derives `inited` from the size the
193        // file had before growing, so the elements below it really are
194        // initialised (they were written by a previous session).
195        unsafe { self.0.grow_filled_exact(cap, value) }
196    }
197}