rawdb/
reader.rs

1use memmap2::MmapMut;
2use parking_lot::RwLockReadGuard;
3
4use crate::{Database, Region, RegionMetadata};
5
6/// Zero-copy reader for accessing region data from memory-mapped storage.
7///
8/// Holds a lock on the memory map and a snapshot of region metadata.
9/// The metadata is cloned at reader creation time, providing snapshot isolation
10/// and avoiding lock ordering deadlocks with writers.
11///
12/// # Important: Lock Duration
13///
14/// **Drop this reader as soon as possible.** While held, this blocks:
15/// - `set_min_len` (file growth)
16/// - `compact` / `punch_holes` (final sync phase)
17///
18/// Long-lived readers can cause other operations to hang waiting for the lock.
19/// If you need to keep data around, copy it out of the reader first.
20///
21/// The Reader owns references to the Database and Region to ensure the
22/// underlying data structures remain valid for the lifetime of the guards.
23#[must_use = "Reader holds locks and should be used for reading"]
24pub struct Reader {
25    // SAFETY: Field order is critical! Rust drops fields in declaration order (first field first).
26    // The Arc-wrapped structures (_db, _region) MUST be declared BEFORE the guards so they are
27    // dropped AFTER the guards. This ensures the RwLock remains valid while the guard exists.
28    // DO NOT REORDER these fields without understanding the safety implications.
29    _db: Database,
30    _region: Region,
31    meta: RegionMetadata,
32    mmap: RwLockReadGuard<'static, MmapMut>,
33}
34
35impl Reader {
36    /// Creates a new Reader for the given region.
37    ///
38    /// Clones the region metadata to provide snapshot isolation and avoid
39    /// lock ordering deadlocks with writers. The metadata lock is held only
40    /// briefly during clone, then released.
41    ///
42    /// # Safety
43    /// This uses transmute to extend the mmap guard lifetime to 'static. This is safe because:
44    /// - The guard borrows from a RwLock inside an Arc-wrapped Database
45    /// - Reader owns a clone of that Arc (_db field)
46    /// - The Arc is dropped AFTER the guard (field declaration order)
47    /// - Therefore the RwLock remains valid for the guard's entire lifetime
48    #[inline]
49    pub(crate) fn new(region: &Region) -> Self {
50        let db = region.db();
51        let region = region.clone();
52
53        // Clone metadata, releasing the lock immediately.
54        // This avoids lock ordering deadlocks with writers who need region.meta().write()
55        // while holding pages.write().
56        let meta = region.meta().clone();
57
58        // SAFETY: The guard borrows from a RwLock inside the Arc-wrapped Database.
59        // We store a clone of this Arc in the Reader struct. Rust drops fields in
60        // declaration order (first field first), and _db is declared BEFORE mmap,
61        // so _db is dropped AFTER mmap. This guarantees the RwLock remains valid
62        // for the entire lifetime of the guard.
63        let mmap: RwLockReadGuard<'static, MmapMut> = unsafe { std::mem::transmute(db.mmap()) };
64
65        Self {
66            _db: db,
67            _region: region,
68            meta,
69            mmap,
70        }
71    }
72
73    /// Reads data from the region without bounds checking.
74    ///
75    /// # Safety
76    /// The caller must ensure `offset + len` is within the region's length.
77    /// Reading beyond the region's bounds is undefined behavior.
78    #[inline(always)]
79    pub fn unchecked_read(&self, offset: usize, len: usize) -> &[u8] {
80        let start = self.start() + offset;
81        let end = start + len;
82        &self.mmap[start..end]
83    }
84
85    /// Reads a slice of data from the region at the given offset.
86    ///
87    /// # Panics
88    /// Panics if `offset + len` exceeds the region's length.
89    #[inline(always)]
90    pub fn read(&self, offset: usize, len: usize) -> &[u8] {
91        assert!(offset + len <= self.len());
92        self.unchecked_read(offset, len)
93    }
94
95    /// Returns the starting offset of this region in the database file.
96    #[inline(always)]
97    fn start(&self) -> usize {
98        self.meta.start()
99    }
100
101    /// Returns the length of data in the region.
102    #[inline(always)]
103    pub fn len(&self) -> usize {
104        self.meta.len()
105    }
106
107    /// Returns true if the region is empty.
108    #[inline(always)]
109    pub fn is_empty(&self) -> bool {
110        self.len() == 0
111    }
112
113    /// Returns a slice containing all data in the region.
114    #[inline(always)]
115    pub fn read_all(&self) -> &[u8] {
116        self.read(0, self.len())
117    }
118
119    /// Returns a slice from the offset to the end of the mmap.
120    ///
121    /// This allows reading beyond the region boundary for performance-critical
122    /// sequential access patterns, but the offset must still be within the region.
123    ///
124    /// # Panics
125    /// Panics if the offset exceeds the region's length.
126    #[inline(always)]
127    pub fn prefixed(&self, offset: usize) -> &[u8] {
128        assert!(
129            offset <= self.len(),
130            "Offset {} exceeds region length {}",
131            offset,
132            self.len()
133        );
134        let start = self.start() + offset;
135        &self.mmap[start..]
136    }
137}