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}