velesdb_core/storage/guard.rs
1//! Zero-copy guard for vector data from mmap storage.
2//!
3//! # Choosing between `as_slice()` / `try_deref()` and `Deref` / `AsRef`
4//!
5//! In **fallible contexts** (anything that returns `Result`), prefer
6//! [`VectorSliceGuard::as_slice()`] or its alias [`VectorSliceGuard::try_deref()`].
7//! They return `Result<&[f32]>` and let callers propagate epoch-mismatch errors
8//! gracefully.
9//!
10//! The `Deref` and `AsRef<[f32]>` implementations exist for ergonomics in
11//! contexts where panicking on epoch mismatch is acceptable (e.g., short-lived
12//! guards within a single function scope where remap cannot happen).
13
14use memmap2::MmapMut;
15use parking_lot::RwLockReadGuard;
16
17/// Zero-copy guard for vector data from mmap storage.
18///
19/// This guard holds a read lock on the mmap and provides direct access
20/// to the vector data without any memory allocation or copy.
21///
22/// # Performance
23///
24/// Using `VectorSliceGuard` instead of `retrieve()` eliminates:
25/// - Heap allocation for the result `Vec<f32>`
26/// - Memory copy from mmap to the new vector
27///
28/// # Example
29///
30/// ```rust,no_run
31/// # use velesdb_core::storage::{MmapStorage, VectorSliceGuard};
32/// # use std::io;
33/// # fn example() -> io::Result<()> {
34/// # let mut storage = MmapStorage::new("/tmp/test", 128)?;
35/// # let id = 1u64;
36/// // Get zero-copy access to a vector
37/// let guard: Option<VectorSliceGuard> = storage.retrieve_ref(id)?;
38/// if let Some(guard) = guard {
39/// // Prefer as_slice() / try_deref() in fallible contexts:
40/// if let Ok(slice) = guard.as_slice() {
41/// // use slice...
42/// }
43/// // Or use Deref in short-lived, non-fallible scopes:
44/// let slice: &[f32] = &*guard;
45/// }
46/// # Ok(())
47/// # }
48/// ```
49use std::sync::atomic::AtomicU64;
50
51/// Zero-copy guard for vector data from mmap storage.
52/// Holds a read-lock on the mmap and validates that the underlying mapping
53/// hasn't been remapped via an *epoch* counter.
54///
55/// # Epoch Validation
56///
57/// The guard captures the epoch at creation and validates it on each access.
58/// If the mmap was remapped (epoch changed), access panics to prevent UB.
59///
60/// The epoch uses wrapping `u64` arithmetic. Overflow is theoretically possible
61/// after 2^64 remaps (~584 years at 1B/sec) but practically irrelevant.
62pub struct VectorSliceGuard<'a> {
63 /// Read guard holding the mmap lock – guarantees the mapping is pinned for the guard lifetime
64 pub(super) _guard: RwLockReadGuard<'a, MmapMut>,
65 /// Pointer to the start of vector data
66 pub(super) ptr: *const f32,
67 /// Number of f32 elements
68 pub(super) len: usize,
69 /// Pointer to the global epoch counter inside `MmapStorage`
70 pub(super) epoch_ptr: &'a AtomicU64,
71 /// Epoch captured at construction
72 pub(super) epoch_at_creation: u64,
73}
74
75// SAFETY: `VectorSliceGuard` is `Send` because it carries read-only mapped data.
76// - Condition 1: `_guard` pins the mapping and prevents concurrent remap mutation.
77// - Condition 2: Epoch checks reject stale pointers after remap.
78// SAFETY: Transferring read-only guard ownership across threads preserves invariants.
79#[allow(clippy::non_send_fields_in_send_ty)]
80unsafe impl Send for VectorSliceGuard<'_> {}
81// SAFETY: `VectorSliceGuard` is `Sync` because shared access is immutable.
82// - Condition 1: Exposed data is `&[f32]` only; no mutable alias is produced.
83// - Condition 2: Underlying map lifetime is tied to `_guard` and epoch validation.
84// SAFETY: Concurrent reads of stable mapped memory are sound.
85#[allow(clippy::non_send_fields_in_send_ty)]
86unsafe impl Sync for VectorSliceGuard<'_> {}
87
88impl VectorSliceGuard<'_> {
89 /// Returns the vector data as a slice.
90 ///
91 /// # Errors
92 ///
93 /// Returns `Error::EpochMismatch` if the underlying mmap has been remapped
94 /// since this guard was created, meaning the pointer is stale.
95 #[inline]
96 pub fn as_slice(&self) -> crate::error::Result<&[f32]> {
97 // SAFETY: ptr and len were validated during construction,
98 // and the guard ensures the mmap remains valid
99 // Verify epoch – if the mmap was remapped the pointer is invalid
100 let current = self.epoch_ptr.load(std::sync::atomic::Ordering::Acquire);
101 if current != self.epoch_at_creation {
102 return Err(crate::error::Error::EpochMismatch(
103 "Mmap was remapped; VectorSliceGuard is invalid".to_string(),
104 ));
105 }
106 // SAFETY: `from_raw_parts` requires a valid pointer/len pair.
107 // - Condition 1: `ptr` and `len` were validated when guard was created.
108 // - Condition 2: Epoch equality above guarantees no remap invalidated `ptr`.
109 // SAFETY: Zero-copy slice access avoids allocations while preserving safety invariants.
110 Ok(unsafe { std::slice::from_raw_parts(self.ptr, self.len) })
111 }
112
113 /// Alias for [`as_slice()`](Self::as_slice) — returns the vector data as
114 /// a fallible reference, matching the naming convention of `std` try-methods.
115 ///
116 /// # Errors
117 ///
118 /// Returns `Error::EpochMismatch` if the underlying mmap has been remapped
119 /// since this guard was created.
120 #[inline]
121 pub fn try_deref(&self) -> crate::error::Result<&[f32]> {
122 self.as_slice()
123 }
124}
125
126impl AsRef<[f32]> for VectorSliceGuard<'_> {
127 /// Returns the vector data as a slice, or an empty slice on epoch mismatch.
128 ///
129 /// # Epoch Mismatch Behavior
130 ///
131 /// If the underlying mmap was remapped after this guard was created, this
132 /// returns `&[]` and logs an error instead of panicking. Callers that need
133 /// to distinguish empty-from-mismatch vs. genuinely-empty should use
134 /// [`as_slice()`](Self::as_slice) which returns `Result`.
135 #[inline]
136 fn as_ref(&self) -> &[f32] {
137 match self.as_slice() {
138 Ok(slice) => slice,
139 Err(e) => {
140 tracing::error!(
141 "VectorSliceGuard::as_ref: epoch mismatch, returning empty slice. \
142 Use as_slice() to handle this gracefully. Error: {e}"
143 );
144 &[]
145 }
146 }
147 }
148}
149
150impl std::ops::Deref for VectorSliceGuard<'_> {
151 type Target = [f32];
152
153 /// Returns the vector data as a slice, or an empty slice on epoch mismatch.
154 ///
155 /// # Epoch Mismatch Behavior
156 ///
157 /// If the underlying mmap was remapped after this guard was created, this
158 /// returns `&[]` and logs an error instead of panicking. Callers that need
159 /// to distinguish empty-from-mismatch vs. genuinely-empty should use
160 /// [`as_slice()`](VectorSliceGuard::as_slice) which returns `Result`.
161 #[inline]
162 fn deref(&self) -> &Self::Target {
163 match self.as_slice() {
164 Ok(slice) => slice,
165 Err(e) => {
166 tracing::error!(
167 "VectorSliceGuard::deref: epoch mismatch, returning empty slice. \
168 Use as_slice() to handle this gracefully. Error: {e}"
169 );
170 &[]
171 }
172 }
173 }
174}