Skip to main content

rushdb_storage/
backend.rs

1//! Narrow storage-substrate seam (research D6, plan W3).
2//!
3//! `StorageBackend` captures the durability/write surface the [`Store`] needs
4//! from its physical substrate. The synchronous redb implementation
5//! (`RedbBackend`) satisfies it today; an async backend (`io_uring` / direct IO)
6//! or a future `RushDB` `PageStore` can replace it without touching LMPG, query,
7//! search, or the replication/transaction layers, because those layers depend
8//! only on this trait and on the concrete read-view type.
9//!
10//! Greenfield note: the read view (`RedbReadView`) remains concrete in V1 — it
11//! is the synchronous backend's owned snapshot. An async backend brings its own
12//! view type behind this same seam; the point of the trait is that the write /
13//! durability contract is already isolated.
14
15use crate::read_view::{ReadLimits, ScanDirection, ScanProjection};
16use crate::resources::ByteReservation;
17use crate::{Op, StorageError, StorageStatistics, VersionEntry};
18use std::collections::BTreeMap;
19use std::ops::Bound;
20use std::path::Path;
21use std::sync::Arc;
22
23/// Object-safe read snapshot of one physical backend.
24///
25/// Both the redb view (`RedbReadView`) and the in-memory view
26/// (`MemoryReadView`) implement it, so [`crate::ReadView`] has one read path
27/// regardless of substrate. Multi-shard stores merge the per-shard
28/// views in `MergedViews`.
29pub(crate) trait StorageReadView: Send + Sync {
30    /// Point read at `ci`, borrowed: when the key is visible, `visit` sees
31    /// the stored value bytes in place (empty for a keys-only projection)
32    /// and the result is `true`; otherwise `visit` is not called. `current`
33    /// selects the latest-commit fast path. This is a backend's one
34    /// implementation of point-read visibility, limits and read accounting;
35    /// [`Self::read_value`] copies out of it. `visit` runs while the backend
36    /// holds its read (a redb page guard, the memory maps' read locks), so it
37    /// must not read the store again.
38    fn visit_value(
39        &self,
40        key: &[u8],
41        ci: u64,
42        current: bool,
43        limits: ReadLimits,
44        projection: ScanProjection,
45        visit: &mut dyn FnMut(&[u8]),
46    ) -> Result<bool, StorageError>;
47
48    /// Point read at `ci` into an owned copy ([`Self::visit_value`]).
49    fn read_value(
50        &self,
51        key: &[u8],
52        ci: u64,
53        current: bool,
54        limits: ReadLimits,
55        projection: ScanProjection,
56    ) -> Result<Option<Vec<u8>>, StorageError> {
57        let mut value = None;
58        self.visit_value(key, ci, current, limits, projection, &mut |raw| {
59            value = Some(raw.to_vec());
60        })?;
61        Ok(value)
62    }
63
64    /// Smallest (`Forward`) or largest (`Reverse`) key in `(lower, upper)`;
65    /// `include_history` widens to retained versions for a non-current
66    /// snapshot.
67    fn next_key(
68        &self,
69        lower: &Bound<Vec<u8>>,
70        upper: &Bound<Vec<u8>>,
71        limits: ReadLimits,
72        include_history: bool,
73        direction: ScanDirection,
74    ) -> Result<Option<Vec<u8>>, StorageError>;
75
76    /// Ordered current-value range with the requested projection.
77    fn current_range<'a>(
78        &'a self,
79        start: &[u8],
80        end: &[u8],
81        limits: ReadLimits,
82        projection: ScanProjection,
83    ) -> Result<Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), StorageError>> + 'a>, StorageError>;
84
85    /// `ScanCursor`'s v2 native-iterator fast path (B7 follow-up): a genuine
86    /// backend range cursor over `(lower, upper)` matching `ScanCursor`'s own
87    /// `Bound` representation exactly, unlike [`Self::current_range`]'s
88    /// simplified included-start/excluded-end-or-unbounded convention. A
89    /// real cursor walks consecutively (one seek for the whole scan, not one
90    /// per row); callers must already know this view's `current` fast path
91    /// is valid — same gate [`Self::visit_value`]'s `current` flag needs,
92    /// since this reads the live current-value table.
93    ///
94    /// `Ok(None)` means this backend has no such cursor; the caller falls
95    /// back to [`Self::next_key`] + [`Self::visit_value`] per row. The
96    /// default is `Ok(None)` so adding this is additive — a backend opts in
97    /// explicitly.
98    fn native_range<'a>(
99        &'a self,
100        _lower: &Bound<Vec<u8>>,
101        _upper: &Bound<Vec<u8>>,
102        _limits: ReadLimits,
103        _projection: ScanProjection,
104    ) -> Result<
105        Option<Box<dyn Iterator<Item = Result<(Vec<u8>, Vec<u8>), StorageError>> + 'a>>,
106        StorageError,
107    > {
108        Ok(None)
109    }
110
111    /// Durable LSN this view can serve.
112    fn visible_ci(&self) -> Result<u64, StorageError>;
113
114    /// Oldest commit for which this view still retains history.
115    fn history_floor(&self) -> Result<u64, StorageError>;
116
117    /// Whether this view is a physically pinned snapshot, so the `current`
118    /// fast path (ignore `ci`, read the live table) stays valid after later
119    /// commits. Redb read transactions pin pages (true); the live in-memory
120    /// maps do not (false), so a view must resolve reads by `ci`.
121    fn snapshot_pinned(&self) -> bool {
122        false
123    }
124}
125
126/// One commit's writes for a backend: its base ops followed by the ops
127/// derived from them, applied at `ci` in the same transaction. The two are
128/// kept as borrowed slices so publication never concatenates them into a
129/// copy of the commit.
130#[derive(Debug, Clone, Copy)]
131pub(crate) struct CommitOps<'a> {
132    pub(crate) ci: u64,
133    pub(crate) base: &'a [Op],
134    pub(crate) derived: &'a [Op],
135}
136
137impl<'a> CommitOps<'a> {
138    /// Base ops then derived ops, in apply order.
139    pub(crate) fn ops(&self) -> impl Iterator<Item = &'a Op> + Clone {
140        let (base, derived) = (self.base, self.derived);
141        base.iter().chain(derived)
142    }
143}
144
145/// Backend-loaded state used to bootstrap a store.
146#[derive(Debug, Default)]
147pub struct LoadedState {
148    /// Current key/value data.
149    pub data: BTreeMap<Vec<u8>, Vec<u8>>,
150    /// Creation commit index per live key.
151    pub created: BTreeMap<Vec<u8>, u64>,
152    /// Retained superseded versions per key.
153    pub history: BTreeMap<Vec<u8>, Vec<VersionEntry>>,
154    /// Durable GC floor.
155    pub gc_watermark: u64,
156}
157
158/// [`StorageBackend::compact`]'s error when a read transaction is open: the
159/// caller retries with every lock released.
160pub(crate) const COMPACT_BUSY: &str = "compaction busy: read transactions open";
161
162/// Write/durability contract of the physical storage substrate.
163///
164/// Implementations must make `apply`/`apply_many` atomic and, when the
165/// configured durability is `Local`, durable before returning. `retire_before`
166/// reclaims retained versions; `compact` releases freed pages.
167///
168/// Visibility is `pub(crate)` in W3: the seam already isolates the write/read
169/// surface so a future async backend (`io_uring` / direct IO) or `RushDB`
170/// `PageStore` can be swapped in without touching LMPG/query/search. The trait
171/// becomes `pub` once an external backend implementation needs to name it.
172pub(crate) trait StorageBackend: Send + Sync {
173    /// Load the full current state (data, MVCC metadata, GC watermark).
174    fn load(&self) -> Result<LoadedState, String>;
175
176    /// Read only the durable-LSN watermark and GC floor: startup needs
177    /// these to restore the committed version WITHOUT materializing the full
178    /// dataset into the RAM mirror. Implementations should read only the two
179    /// metadata keys; the default falls back to a full `load`.
180    fn load_watermark(&self) -> Result<(u64, u64), String> {
181        let state = self.load()?;
182        let lsn = match state.data.get(crate::LAST_APPLIED_KEY) {
183            Some(value) => crate::wal::decode_last_applied(value)
184                .map_err(|error| format!("corrupt last-applied watermark: {error}"))?,
185            None => 0,
186        };
187        Ok((lsn, state.gc_watermark))
188    }
189
190    /// Apply one logical op-set at `commit_index`.
191    fn apply(&self, ops: &[Op], commit_index: u64) -> Result<(), String>;
192
193    /// Apply many commits (ascending commit-index order) in ONE write
194    /// transaction: a single durable acknowledgement covers the whole batch.
195    fn apply_many(&self, batches: &[CommitOps<'_>]) -> Result<(), String>;
196
197    /// Reclaim retained versions superseded before `watermark`.
198    fn retire_before(&self, watermark: u64) -> Result<(), String>;
199
200    /// Reclaim a bounded batch of history keys superseded before `watermark`,
201    /// resuming after `cursor`. Returns the next key to resume from, or `None`
202    /// when the walk is complete. Each call is one write transaction, so GC is
203    /// bounded and resumable instead of one monolithic txn.
204    fn retire_before_batch(
205        &self,
206        watermark: u64,
207        _cursor: Option<Vec<u8>>,
208        _max_keys: usize,
209    ) -> Result<Option<Vec<u8>>, String> {
210        // Default: fall back to the monolithic reclamation. Implementations
211        // that support bounded GC override this.
212        self.retire_before(watermark)?;
213        Ok(None)
214    }
215
216    /// Signal in-flight maintenance scans to stop promptly (shutdown). Backends
217    /// with no long scans treat this as a no-op.
218    fn begin_shutdown(&self) {}
219
220    /// Force queued writes durable (fsync) without changing logical state, and
221    /// durably record `durable_lsn` as the materialization checkpoint. Under
222    /// log-first the WAL is the durable truth and the backend is buffered, so a
223    /// checkpoint is what makes the materialization durable and lets WAL
224    /// segments at or below `durable_lsn` be truncated.
225    ///
226    /// Returns whether anything was written: a checkpoint with no buffered
227    /// write since the last one, at the same `durable_lsn`, writes nothing.
228    /// Backends with no buffered writes always return `false`.
229    fn checkpoint(&self, _durable_lsn: u64) -> Result<bool, String> {
230        Ok(false)
231    }
232
233    /// Run `run` while this backend commits nothing (readers continue), so
234    /// its file can be copied consistently. Backends without a file just run
235    /// it.
236    fn while_writes_paused(&self, run: &mut dyn FnMut()) -> Result<(), String> {
237        run();
238        Ok(())
239    }
240
241    /// Compact the backend, releasing freed pages. Returns [`COMPACT_BUSY`]
242    /// when a read transaction is open; it never waits for one.
243    fn compact(&self) -> Result<(), String>;
244
245    /// Durable byte accounting by key family. Default implementations return
246    /// an explicit error (unavailable); file-backed backends override it.
247    fn storage_stats(&self) -> Result<StorageStatistics, String> {
248        Err("backend does not provide storage statistics".into())
249    }
250
251    /// Open an owned read view (snapshot) of the current committed state.
252    fn read_view(&self) -> Result<Box<dyn StorageReadView>, StorageError>;
253
254    /// Cumulative time spent waiting for and holding the backend's database
255    /// lock. Backends without such a lock report zeros.
256    fn lock_times(&self) -> crate::BackendLockTimes {
257        crate::BackendLockTimes::default()
258    }
259    /// Writer holds by holder. Backends without a single writer
260    /// report none.
261    fn writer_holds(&self) -> Vec<(crate::WriterHolder, crate::WriterHolds)> {
262        Vec::new()
263    }
264}
265
266/// Open the redb backend as a [`StorageBackend`] handle.
267pub(crate) fn open_backend(path: &Path, cache: ByteReservation) -> Result<BackendHandle, String> {
268    Ok(Arc::new(crate::redb_backend::RedbBackend::open(
269        path, cache,
270    )?))
271}
272
273/// Convenience alias used by [`Store`] for its durable backend handle.
274pub(crate) type BackendHandle = Arc<dyn StorageBackend>;