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}