Skip to main content

hexz_core/store/local/
mmap.rs

1//! Memory-mapped file storage backend.
2//!
3//! This module implements the `StorageBackend` trait using `mmap(2)` to map the
4//! entire snapshot file into the process's virtual address space. This approach
5//! delegates paging, caching, and prefetching to the kernel's virtual memory
6//! subsystem, often providing superior performance for random-access workloads
7//! compared to traditional file I/O.
8//!
9//! # Architecture
10//!
11//! The [`MmapBackend`] maps the entire file into memory using `MAP_PRIVATE` and
12//! `PROT_READ` flags. The mapping is read-only and copy-on-write (though no writes
13//! occur). The `Mmap` handle is wrapped in `Arc<Mmap>` to enable thread-safe
14//! shared access across multiple readers.
15//!
16//! Key characteristics:
17//! - **Zero-copy reads**: Data is accessed directly from mapped pages
18//! - **Lazy loading**: Pages are only loaded from disk when accessed (demand paging)
19//! - **Transparent caching**: The OS page cache automatically retains hot pages
20//! - **Automatic prefetch**: Sequential access patterns trigger kernel read-ahead
21//!
22//! # Safety Considerations
23//!
24//! Memory mapping is inherently unsafe because:
25//! - **File modification**: If another process truncates or modifies the file while
26//!   mapped, accessing those pages can trigger `SIGBUS` (segmentation fault)
27//! - **Race conditions**: Concurrent file modifications can cause undefined behavior
28//!
29//! This backend assumes **immutable snapshot semantics**: the file must not be
30//! modified by any process while the backend is active. Violating this assumption
31//! can cause process crashes or data corruption.
32//!
33//! # Thread Safety
34//!
35//! The backend is fully thread-safe (`Send + Sync`):
36//! - The `Mmap` is wrapped in `Arc` for safe shared ownership
37//! - Reads are lock-free (simple `memcpy` from mapped pages)
38//! - The kernel handles concurrent page faults transparently
39//!
40//! Multiple threads can safely read from the same backend simultaneously without
41//! coordination or contention.
42//!
43//! # Performance Characteristics
44//!
45//! - **Latency (resident pages)**: 1-5µs (direct memory access)
46//! - **Latency (non-resident pages)**: 5-50µs (page fault + disk I/O)
47//! - **Throughput**: Up to memory bandwidth (~10-50GB/s for resident data)
48//! - **CPU overhead**: Zero-copy for resident pages, page fault handler for cold pages
49//! - **Memory overhead**: OS transparently manages resident set size; can evict under pressure
50//!
51//! # When to Use This Backend
52//!
53//! Prefer [`MmapBackend`] over [`FileBackend`](super::file::FileBackend) when:
54//! - Working with small to medium files (<1GB) that fit mostly in RAM
55//! - Access patterns exhibit strong spatial locality (e.g., scanning index structures)
56//! - You want to leverage OS-level prefetching and caching heuristics
57//! - Minimizing CPU overhead is critical (zero-copy access)
58//!
59//! Avoid [`MmapBackend`] when:
60//! - Files are very large (>10GB) with sparse access (wastes address space)
61//! - Running in restricted environments where `mmap(2)` is disabled
62//! - Profiling shows excessive page faults or thrashing
63//! - You need explicit control over buffering and I/O scheduling
64//!
65//! # Platform Support
66//!
67//! This backend uses the `memmap2` crate, which supports:
68//! - **Linux**: Full support via `mmap(2)`
69//! - **macOS**: Full support via `mmap(2)`
70//! - **Windows**: Supported via `MapViewOfFile` (not tested in this module)
71//!
72//! # Error Handling
73//!
74//! Errors can occur during:
75//! - **Construction**: File open failure, insufficient address space, `mmap(2)` failure
76//! - **Reads**: Out-of-bounds access (checked in software)
77//!
78//! Note: If the mapped file is modified externally, the kernel may deliver `SIGBUS`,
79//! which this code cannot catch. Always ensure files are immutable.
80//!
81//! # Examples
82//!
83//! ```no_run
84//! use hexz_core::store::local::MmapBackend;
85//! use hexz_core::store::StorageBackend;
86//! use std::path::Path;
87//!
88//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
89//! // Open and map a snapshot file
90//! let backend = MmapBackend::new(Path::new("/data/snapshot.hxz"))?;
91//!
92//! // Read 4KB starting at offset 8192
93//! let data = backend.read_exact(8192, 4096)?;
94//! assert_eq!(data.len(), 4096);
95//!
96//! // Concurrent reads from multiple threads
97//! let backend = std::sync::Arc::new(backend);
98//! let handles: Vec<_> = (0..4)
99//!     .map(|i| {
100//!         let b = backend.clone();
101//!         std::thread::spawn(move || {
102//!             b.read_exact(i * 1024, 1024)
103//!         })
104//!     })
105//!     .collect();
106//!
107//! for handle in handles {
108//!     let result = handle.join().unwrap()?;
109//!     assert_eq!(result.len(), 1024);
110//! }
111//! # Ok(())
112//! # }
113//! ```
114
115use crate::store::StorageBackend;
116use bytes::Bytes;
117use hexz_common::Result;
118use memmap2::Mmap;
119use std::fs::File;
120use std::sync::Arc;
121
122/// A storage backend backed by a memory-mapped file.
123///
124/// This struct holds a reference to the memory map. Accessing data is as efficient
125/// as a memory copy (`memcpy`), avoiding the explicit system call overhead of
126/// `read` or `pread`. The OS handles paging data in from disk as needed.
127#[derive(Debug)]
128pub struct MmapBackend {
129    /// The memory map, wrapped in an `Arc` for thread-safe shared ownership.
130    map: Arc<Mmap>,
131    /// The total size of the mapped file in bytes.
132    len: u64,
133}
134
135impl MmapBackend {
136    /// Opens a snapshot file and maps it into the process address space.
137    ///
138    /// This constructor performs three operations:
139    /// 1. Opens the file at `path` in read-only mode
140    /// 2. Queries the file size via `fstat(2)`
141    /// 3. Creates a read-only memory mapping (`MAP_PRIVATE | PROT_READ`)
142    ///
143    /// The mapping is private (copy-on-write), but since no writes occur, it
144    /// effectively shares the underlying page cache with other processes reading
145    /// the same file.
146    ///
147    /// # Parameters
148    ///
149    /// - `path`: Filesystem path to the snapshot file (absolute or relative)
150    ///
151    /// # Returns
152    ///
153    /// - `Ok(MmapBackend)`: Successfully mapped and initialized
154    /// - `Err(Error::Io)`: If the file cannot be opened or mapped
155    ///
156    /// # Errors
157    ///
158    /// Common error conditions:
159    /// - **File not found** (`ENOENT`): Path does not exist
160    /// - **Permission denied** (`EACCES`): Insufficient permissions to read file
161    /// - **Out of memory** (`ENOMEM`): Insufficient virtual address space (rare on 64-bit)
162    /// - **Invalid file**: Cannot map special files (e.g., `/dev/null`, pipes, sockets)
163    ///
164    /// # Safety
165    ///
166    /// This function uses `unsafe { Mmap::map(&file) }` internally. The safety
167    /// invariant is that the mapped file must not be modified or truncated by any
168    /// process while the mapping exists. Violating this invariant can cause:
169    /// - **SIGBUS**: Accessing pages corresponding to truncated regions
170    /// - **Data races**: Undefined behavior if file contents change during reads
171    ///
172    /// This backend enforces immutable snapshot semantics: the caller must ensure
173    /// the file is not modified after construction.
174    ///
175    /// # Performance
176    ///
177    /// The mapping operation is lazy. This constructor only reserves virtual address
178    /// space; no disk I/O occurs until pages are accessed. For a 1GB file:
179    /// - Constructor latency: ~100µs (no I/O, just syscall overhead)
180    /// - First read latency: 5-50µs per 4KB page (demand paging)
181    ///
182    /// # Examples
183    ///
184    /// ```no_run
185    /// use hexz_core::store::local::MmapBackend;
186    /// use std::path::Path;
187    ///
188    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
189    /// // Absolute path
190    /// let backend = MmapBackend::new(Path::new("/var/data/snapshot.hxz"))?;
191    ///
192    /// // Relative path
193    /// let backend = MmapBackend::new(Path::new("./snapshots/test.hxz"))?;
194    ///
195    /// // Error handling
196    /// match MmapBackend::new(Path::new("/nonexistent.hxz")) {
197    ///     Ok(_) => println!("Success"),
198    ///     Err(e) => eprintln!("Failed to map: {}", e),
199    /// }
200    /// # Ok(())
201    /// # }
202    /// ```
203    pub fn new(path: &std::path::Path) -> Result<Self> {
204        let file = File::open(path)?;
205        let len = file.metadata()?.len();
206        let map = unsafe { Mmap::map(&file)? };
207        Ok(Self {
208            map: Arc::new(map),
209            len,
210        })
211    }
212}
213
214impl StorageBackend for MmapBackend {
215    /// Reads exactly `len` bytes starting at `offset` from the mapped file.
216    ///
217    /// This method performs a `memcpy` from the mapped region into a `Bytes` buffer.
218    /// If the requested pages are not resident in RAM, the CPU will trigger a page
219    /// fault, and the kernel will transparently load the data from disk (demand paging).
220    ///
221    /// # Parameters
222    ///
223    /// - `offset`: Absolute byte offset from the start of the file (0-indexed)
224    /// - `len`: Number of bytes to read (must not cause `offset + len` to exceed file size)
225    ///
226    /// # Returns
227    ///
228    /// - `Ok(Bytes)`: A buffer containing exactly `len` bytes of data
229    /// - `Err(Error::Io)`: If the read exceeds file boundaries
230    ///
231    /// # Errors
232    ///
233    /// This method returns an error if:
234    /// - **Out of bounds** (`ErrorKind::UnexpectedEof`): `offset + len > file_size`
235    ///
236    /// Note: If the mapped file is modified externally (violating immutability),
237    /// accessing those pages may trigger `SIGBUS`, which cannot be caught by this code.
238    ///
239    /// # Performance
240    ///
241    /// - **Time complexity**: O(len) for `memcpy`, O(1) for bounds check
242    /// - **Syscalls**: 0 if pages are resident, page fault handler if not resident
243    /// - **Allocations**: 1 heap allocation of `len` bytes for the returned `Bytes`
244    /// - **CPU overhead**: ~10-50 CPU cycles per byte for resident pages
245    ///
246    /// **Latency breakdown** (4KB read):
247    /// - Resident pages: ~1-5µs (pure memory copy)
248    /// - Non-resident pages: ~5-50µs (page fault + disk I/O)
249    ///
250    /// # Concurrency
251    ///
252    /// This method is safe to call concurrently from multiple threads. The kernel
253    /// handles concurrent page faults transparently. Reads never block each other
254    /// unless they trigger page faults for the same pages simultaneously (rare and
255    /// efficiently handled by the kernel).
256    ///
257    /// # Examples
258    ///
259    /// ```no_run
260    /// use hexz_core::store::local::MmapBackend;
261    /// use hexz_core::store::StorageBackend;
262    /// use std::path::Path;
263    ///
264    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
265    /// let backend = MmapBackend::new(Path::new("/data/snapshot.hxz"))?;
266    ///
267    /// // Read first 512 bytes (header)
268    /// let header = backend.read_exact(0, 512)?;
269    /// assert_eq!(header.len(), 512);
270    ///
271    /// // Read 4KB block at offset 1MB
272    /// let block = backend.read_exact(1024 * 1024, 4096)?;
273    /// assert_eq!(block.len(), 4096);
274    ///
275    /// // Error: reading beyond file boundary
276    /// let file_size = backend.len();
277    /// assert!(backend.read_exact(file_size, 1).is_err());
278    /// # Ok(())
279    /// # }
280    /// ```
281    fn read_exact(&self, offset: u64, len: usize) -> Result<Bytes> {
282        let start = offset as usize;
283        let end = start + len;
284
285        if end > self.map.len() {
286            return Err(std::io::Error::new(
287                std::io::ErrorKind::UnexpectedEof,
288                "Read out of bounds",
289            )
290            .into());
291        }
292
293        Ok(Bytes::copy_from_slice(&self.map[start..end]))
294    }
295
296    /// Returns the total mapped file size in bytes.
297    ///
298    /// This value is cached during construction via `File::metadata()` and reflects
299    /// the file size at the time the mapping was created. The file is assumed to be
300    /// immutable; if the file is truncated or extended externally, this value will
301    /// not be updated and accessing changed regions may cause undefined behavior.
302    ///
303    /// # Returns
304    ///
305    /// The file size in bytes as of the time `MmapBackend::new()` was called.
306    ///
307    /// # Performance
308    ///
309    /// This method is a simple field access with no system calls (O(1)).
310    ///
311    /// # Examples
312    ///
313    /// ```no_run
314    /// use hexz_core::store::local::MmapBackend;
315    /// use hexz_core::store::StorageBackend;
316    /// use std::path::Path;
317    ///
318    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
319    /// let backend = MmapBackend::new(Path::new("/data/snapshot.hxz"))?;
320    /// let size = backend.len();
321    /// println!("Snapshot size: {} bytes ({} MB)", size, size / 1024 / 1024);
322    /// # Ok(())
323    /// # }
324    /// ```
325    fn len(&self) -> u64 {
326        self.len
327    }
328}