rushdb_storage/lib.rs
1//! Ordered KV substrate shared by every node: buffered transactions over a
2//! committed B-tree view, atomic redb publication of expanded op sets, a
3//! serialized command log, multi-version snapshot reads, and
4//! serializable-optimistic dependency validation.
5
6// The storage boundary deliberately exposes byte-oriented iterator and callback
7// signatures. Naming each nested trait-object form would obscure that API.
8#![allow(clippy::type_complexity)]
9
10mod adapter_budgets;
11pub mod backend;
12pub mod cache;
13mod health;
14pub mod read_accounting;
15mod transaction_view;
16pub use transaction_view::Txn;
17use transaction_view::TxnBase;
18mod commit;
19mod copy_on_write;
20pub use copy_on_write::CopyOnWrite;
21mod conflict_window;
22mod derive;
23mod fair_gate;
24pub mod history_v2;
25mod layout;
26mod log_export;
27pub mod log_store;
28mod maintenance;
29mod memory_backend;
30mod observability;
31mod overlay_view;
32pub mod owner_sets;
33mod persist;
34pub mod planner_stats;
35mod publish;
36mod read_view;
37mod receipt;
38mod redb_backend;
39pub mod resources;
40mod temp_path;
41pub mod tenant_dir;
42mod usage;
43pub use temp_path::TempPath;
44pub use usage::is_usage_bytes_key;
45pub mod wal;
46mod wal_replay;
47use backend::StorageBackend;
48pub use derive::{DeriveRead, Deriver};
49pub use maintenance::CompactOutcome;
50pub use observability::{
51 BackendLockTimes, CommitObserver, CommitPhase, WriterHolder, WriterHolds, WRITER_HOLD_BOUNDS,
52};
53pub use overlay_view::OverlayView;
54pub use planner_stats::PlannerStats;
55pub use read_view::{
56 ReadCancellation, ReadLimits, ReadPin, ReadView, ScanBatch, ScanBounds, ScanCursor,
57 ScanProjection, ScanRow,
58};
59
60#[cfg(test)]
61mod derive_tests;
62#[cfg(test)]
63mod read_view_tests;
64#[cfg(test)]
65mod receipt_tests;
66#[cfg(test)]
67mod regression_tests;
68#[cfg(test)]
69mod sequencer_tests;
70#[cfg(test)]
71mod writer_tests;
72
73/// Remove the materialization file at `path`. The WAL directory and layout
74/// manifest are left alone.
75///
76/// # Errors
77/// Returns the I/O error of the removal.
78pub fn remove_store(path: &std::path::Path) -> std::io::Result<()> {
79 std::fs::remove_file(path)
80}
81
82use rushdb_types::command::CommitBatch;
83use rushdb_types::ids::TenantId;
84use serde::de::DeserializeOwned;
85use serde::Serialize;
86use std::cell::RefCell;
87use std::collections::{BTreeMap, BTreeSet};
88use std::ops::Bound::{Included, Unbounded};
89use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
90use std::sync::mpsc;
91use std::sync::{Arc, Condvar, Mutex, RwLock};
92use thiserror::Error;
93use uuid::Uuid;
94
95/// Per-tenant outer admission shards, retained for replication call ordering.
96/// Local expansion additionally takes the single logical writer mutex.
97const N_SHARDS: usize = 64;
98
99/// A fully-admitted op-set awaiting batched durable publication, keyed by
100/// its reserved commit index. The publisher drains the contiguous prefix from
101/// the published watermark in one redb write transaction.
102struct PendingCommit {
103 ci: u64,
104 /// Physical shard this commit belongs to (tenant routing). The publisher
105 /// fans out durability across shard backends so different tenants persist
106 /// in parallel once `shard_count > 1`.
107 shard: usize,
108 ops: Vec<Op>,
109 prepared: PreparedCommand,
110 /// The originating `CommitBatch`, pre-serialized: paired with
111 /// `ops` in the WAL record so the command journal is derivable from the
112 /// WAL alone, without a separate `__log__` redb row.
113 command_payload: Vec<u8>,
114 ack: mpsc::Sender<Result<(), StorageError>>,
115}
116
117/// Pending-overlay state for the commit sequencer.
118///
119/// A committed `base` plus the net key effect of every assigned-but-unpublished
120/// commit, so an expansion can read through `Txn`'s overlay instead of waiting
121/// for the predecessor to be published. It is guarded by the writer
122/// `admission_lock`, so every writer that can assign a commit index observes
123/// one consistent overlay.
124///
125/// `buffer` is this state's *own* copy (never the publisher's `pending` map), so
126/// it cannot race the publisher removing a drained entry. Rebase only needs the
127/// generation (commit index) at which each key was last written, so the buffer
128/// stays O(distinct in-flight keys), not O(in-flight ops).
129#[derive(Default)]
130struct SequencerState {
131 /// `key -> (commit index that last wrote it, value or tombstone)` for
132 /// assigned-but-unpublished commits. The committed base is opened fresh
133 /// (and dropped) per critical section, so this state holds no read pin
134 /// between commits and cannot stall GC.
135 buffer: BTreeMap<Vec<u8>, (u64, Option<Vec<u8>>)>,
136}
137
138impl std::fmt::Debug for SequencerState {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("SequencerState")
141 .field("in_flight_keys", &self.buffer.len())
142 .finish()
143 }
144}
145
146impl SequencerState {
147 /// Fold an assigned commit's expanded ops into the pending buffer.
148 fn record(&mut self, ci: u64, ops: &[Op]) {
149 for op in ops {
150 match op {
151 Op::Put(key, value) | Op::Set(key, value) => {
152 self.buffer.insert(key.clone(), (ci, Some(value.clone())));
153 }
154 Op::Del(key) | Op::Purge(key) => {
155 self.buffer.insert(key.clone(), (ci, None));
156 }
157 }
158 }
159 }
160}
161
162pub(crate) const LOG_PREFIX: &[u8] = b"__log__/";
163const LAST_APPLIED_KEY: &[u8] = b"meta/last-applied";
164/// Durable watermark: the highest LSN retention has retired.
165/// Bounds `Store::truncate_log`, so the retention floor (hard caps and
166/// consumer protection) decides which WAL segments may go.
167pub(crate) const RETENTION_CURSOR_KEY: &[u8] = b"meta/retention-cursor";
168
169/// Durable byte accounting by key family, for growth diagnosis and operator
170/// metrics. All values are exact sums of stored key+value lengths for the
171/// family, taken from one read view; they exclude redb's copy-on-write slack
172/// (compare against [`Store::storage_file_bytes`]).
173#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
174pub struct StorageStatistics {
175 /// Canonical data rows (records, occurrences, postings, catalogs, …).
176 pub data_keys: u64,
177 /// Total key+value bytes of canonical data rows.
178 pub data_bytes: u64,
179 /// Retained MVCC history rows.
180 pub history_keys: u64,
181 /// Total key+encoded-history bytes.
182 pub history_bytes: u64,
183 /// Retained command-log rows (`__log__/<lsn>`).
184 pub log_keys: u64,
185 /// Total command-log bytes.
186 pub log_bytes: u64,
187 /// Retry-receipt rows (receipts plus their expiry index).
188 pub receipt_keys: u64,
189 /// Total retry-receipt bytes.
190 pub receipt_bytes: u64,
191 /// Number of bounded admission-lock waits (including timed-out waits).
192 pub admission_waits: u64,
193 /// Milliseconds spent waiting for admission-lock ownership.
194 pub admission_wait_ms: u64,
195 /// Number of explicit durability checkpoints.
196 pub checkpoints: u64,
197 /// Milliseconds spent in explicit durability checkpoints.
198 pub checkpoint_wait_ms: u64,
199 /// Bytes appended to the WAL (framed records), cumulative since open.
200 pub wal_bytes_appended: u64,
201 /// Encoded bytes of every op, base and derived, handed to the backends,
202 /// cumulative since open. With `wal_bytes_appended`, the two write
203 /// streams a commit produces; compare against `rushdb.redb` growth and
204 /// the process's block writes to attribute write amplification.
205 pub applied_bytes: u64,
206}
207
208/// Errors returned by the storage layer: failed preconditions, corrupt or
209/// unsupported durable state, resource exhaustion, and invalid read requests.
210#[derive(Debug, Clone, Error)]
211pub enum StorageError {
212 /// A poisoned storage lock fenced the store; reopen is required.
213 #[error("storage unhealthy after lock poisoning; reopen required")]
214 Unhealthy,
215 /// A transaction precondition was violated; `path` is the offending key.
216 #[error("precondition failed on {path}")]
217 Precondition {
218 /// Key whose precondition failed.
219 path: String,
220 },
221 /// The deterministic expander failed while building a commit.
222 #[error("builder error: {0}")]
223 Builder(String),
224 /// A durable record could not be decoded or verified.
225 #[error("corrupt log entry")]
226 CorruptLog,
227 /// A log record carries a command version this build does not support.
228 #[error("unsupported command version {0}")]
229 UnsupportedCommandVersion(u32),
230 /// The stored outcome payload is missing or cannot be deserialized.
231 #[error("committed outcome payload is unavailable or incompatible")]
232 OutcomePayload,
233 /// The physical backend failed while applying or reading durable state.
234 #[error("durable storage failure: {0}")]
235 Durability(String),
236 /// The requested snapshot predates the oldest retained commit.
237 #[error("snapshot {requested} expired; oldest supported commit is {oldest}")]
238 SnapshotExpired {
239 /// Snapshot commit index that was requested.
240 requested: u64,
241 /// Oldest commit index still readable.
242 oldest: u64,
243 },
244 /// The requested snapshot is newer than the visible commit index.
245 #[error("snapshot {requested} is newer than visible commit {visible}")]
246 FutureSnapshot {
247 /// Snapshot commit index that was requested.
248 requested: u64,
249 /// Commit index currently visible.
250 visible: u64,
251 },
252 /// A managed byte budget refused the request.
253 #[error("storage resource exhausted: {0}")]
254 ResourceExhausted(String),
255 /// The supplied storage configuration is not valid.
256 #[error("invalid storage configuration: {0}")]
257 InvalidConfiguration(String),
258 /// A pinned read was cancelled by its caller.
259 #[error("storage read cancelled")]
260 Cancelled,
261 /// Scan bounds or batch limits are invalid.
262 #[error("invalid scan bounds or limits: {0}")]
263 InvalidRead(String),
264 /// Physical compaction was requested while protected read views are open.
265 #[error("physical compaction requires all protected read views to close")]
266 ReadersActive,
267 /// No conflict summary is retained for a commit that must be probed.
268 #[error(
269 "actual write summary unavailable for commit {commit_index}; retry with a fresh snapshot"
270 )]
271 ConflictHistoryUnavailable {
272 /// Commit index whose conflict summary is missing.
273 commit_index: u64,
274 },
275}
276
277/// Operation emitted into the atomic publish set.
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum Op {
280 /// Insert or overwrite a key, retaining the superseded value as history.
281 Put(Vec<u8>, Vec<u8>),
282 /// Remove a key, retaining the removed value as history.
283 Del(Vec<u8>),
284 /// Overwrite a key **without** retaining a superseded version (and drop
285 /// any history already accumulated for it). Used for internal bookkeeping
286 /// keys whose values are not part of user-visible time travel: counters,
287 /// allocation watermarks, degree counters, the label catalog and the
288 /// commit watermark. Without this, every overwrite of a hot counter
289 /// rewrites its whole history vector, which is quadratic in the number of
290 /// writes.
291 Set(Vec<u8>, Vec<u8>),
292 /// Erase a key together with its current-value MVCC metadata and retained
293 /// history, without appending a new version. Destructive by design: used
294 /// by tenant purge, where the values are intentionally destroyed and
295 /// keeping versions for them would bloat storage without bound.
296 Purge(Vec<u8>),
297}
298
299impl Op {
300 /// Approximate encoded size (key + value bytes) for group-commit byte
301 /// accounting (P04 bounded writer).
302 #[must_use]
303 pub fn encoded_len(&self) -> usize {
304 match self {
305 Op::Put(k, v) | Op::Set(k, v) => k.len() + v.len(),
306 Op::Del(k) | Op::Purge(k) => k.len(),
307 }
308 }
309}
310
311/// Reason prefix of a [`CommitOutcome::Conflicted`] caused by a read
312/// dependency that a later commit wrote (first-committer-wins).
313pub const SERIALIZABLE_CONFLICT: &str = "serializable_conflict";
314
315/// Serializable-mode read dependencies recorded against a transaction.
316/// Zero-width entries are point reads; otherwise half-open [start,end).
317#[derive(Default, Clone, Debug)]
318pub struct DependencySet {
319 /// Recorded read ranges as `(start, end)` pairs; a zero-width pair is a
320 /// point dependency.
321 pub ranges: Vec<(Vec<u8>, Vec<u8>)>,
322 whole_keyspace: bool,
323}
324
325impl DependencySet {
326 /// Whether no point, range, or whole-keyspace dependency was recorded.
327 #[must_use]
328 pub fn is_empty(&self) -> bool {
329 self.ranges.is_empty() && !self.whole_keyspace
330 }
331
332 /// Record a half-open `[start, end)` range dependency.
333 pub fn record(&mut self, start: Vec<u8>, end: Vec<u8>) {
334 self.ranges.push((start, end));
335 }
336
337 /// Prefix tracking is explicit so an empty prefix (all keys) is distinct
338 /// from a point dependency on the empty key.
339 pub fn record_prefix(&mut self, prefix: &[u8]) {
340 if prefix.is_empty() {
341 self.whole_keyspace = true;
342 } else {
343 let (start, end) = prefix_range(prefix);
344 self.record(start, end);
345 }
346 }
347
348 /// Merge dependencies captured by another preparation phase.
349 pub fn extend(&mut self, other: Self) {
350 self.ranges.extend(other.ranges);
351 self.whole_keyspace |= other.whole_keyspace;
352 }
353
354 fn intersects(&self, key: &[u8]) -> bool {
355 self.whole_keyspace
356 || self.ranges.iter().any(|(s, e)| {
357 if s == e {
358 key == s.as_slice()
359 } else {
360 key >= s.as_slice() && (e.is_empty() || key < e.as_slice())
361 }
362 })
363 }
364
365 /// Whether `key` falls in any recorded read range (serializable conflict
366 /// probe for transaction commit).
367 #[must_use]
368 pub fn intersects_key(&self, key: &[u8]) -> bool {
369 self.intersects(key)
370 }
371}
372
373/// Prepared metadata for one admitted command. The publisher keeps this
374/// alongside expanded operations so an admission overlay can validate
375/// read/write conflicts without re-expanding the command.
376#[derive(Clone, Debug, Default)]
377pub struct PreparedCommand {
378 /// Reserved commit index, preserving command-log order.
379 pub commit_index: u64,
380 /// Canonical keys written by the expanded operation set.
381 pub writes: BTreeSet<Vec<u8>>,
382 /// Keys/ranges read while preparing the command.
383 pub reads: DependencySet,
384 /// Expanded under writer admission against the sequencer overlay, so its
385 /// reads already observed every lower-index commit (published or
386 /// pending). Such a command can share a publish group with the commits
387 /// it read from: the group applies in commit-index order.
388 pub sequenced: bool,
389}
390
391impl PreparedCommand {
392 /// Build prepared metadata from an expanded operation set.
393 #[must_use]
394 pub fn from_ops(commit_index: u64, ops: &[Op]) -> Self {
395 let writes = ops
396 .iter()
397 .map(|op| match op {
398 Op::Put(key, _) | Op::Set(key, _) | Op::Del(key) | Op::Purge(key) => key,
399 })
400 .cloned()
401 .collect();
402 Self {
403 commit_index,
404 writes,
405 reads: DependencySet::default(),
406 sequenced: false,
407 }
408 }
409
410 /// Whether this command's reads overlap writes from `other`, or vice
411 /// versa. The check is symmetric and suitable for optimistic admission.
412 ///
413 /// The write-write check (large-transaction support, Part F) uses
414 /// `writes` being a `BTreeSet` directly — `other.writes.contains(key)`
415 /// is O(log n), not the nested linear scan this used to be. Filtering
416 /// only `self`'s side is still exactly equivalent to filtering both:
417 /// `is_coordination_write` is a pure function of the key's own bytes, so
418 /// if `key` already passed the filter, an identical byte sequence in
419 /// `other.writes` has the same classification — there is no case where
420 /// filtering the other side too would change the answer.
421 ///
422 /// Read sets are probed **against** the ordered write set — one range
423 /// query per recorded read — rather than testing every written key
424 /// against a linear list of read ranges. The old form was
425 /// O(writes × reads) per pair and, applied pairwise inside every publish
426 /// group, starved bulk ingest for minutes (cloud-capacity F4 follow-up).
427 #[must_use]
428 pub fn conflicts_with(&self, other: &Self) -> bool {
429 reads_hit_writes(&other.reads, &self.writes)
430 || reads_hit_writes(&self.reads, &other.writes)
431 || {
432 let (small, large) = if self.writes.len() <= other.writes.len() {
433 (&self.writes, &other.writes)
434 } else {
435 (&other.writes, &self.writes)
436 };
437 small
438 .iter()
439 .filter(|key| !is_coordination_write(key))
440 .any(|key| large.contains(key))
441 }
442 }
443}
444
445/// Whether any recorded read (point, range, or whole keyspace) covers a
446/// non-coordination key of `writes`. O(reads × log writes).
447fn reads_hit_writes(reads: &DependencySet, writes: &BTreeSet<Vec<u8>>) -> bool {
448 use std::ops::Bound::Excluded;
449 let application = |key: &&Vec<u8>| !is_coordination_write(key);
450 if reads.whole_keyspace {
451 return writes.iter().any(|key| application(&key));
452 }
453 reads.ranges.iter().any(|(start, end)| {
454 if start == end {
455 writes.contains(start) && !is_coordination_write(start)
456 } else if end.is_empty() {
457 writes
458 .range::<[u8], _>((Included(start.as_slice()), Unbounded))
459 .any(|key| application(&key))
460 } else if start < end {
461 writes
462 .range::<[u8], _>((Included(start.as_slice()), Excluded(end.as_slice())))
463 .any(|key| application(&key))
464 } else {
465 false
466 }
467 })
468}
469
470/// Ordered publication owns these metadata rows; they are rewritten by every
471/// commit as part of the command log and do not represent application-level
472/// read/write overlap for group admission.
473fn is_coordination_write(key: &[u8]) -> bool {
474 key == LAST_APPLIED_KEY
475 || key.starts_with(LOG_PREFIX)
476 || key.starts_with(receipt::RECEIPT_PREFIX)
477 || key.starts_with(receipt::RECEIPT_EXP_PREFIX)
478}
479
480/// Inclusive start / exclusive end range covering all keys under a prefix.
481/// An empty end bound means unbounded above.
482#[must_use]
483pub fn prefix_range(prefix: &[u8]) -> (Vec<u8>, Vec<u8>) {
484 let mut end = prefix.to_vec();
485 loop {
486 match end.last_mut() {
487 Some(last) if *last != 0xFF => {
488 *last += 1;
489 return (prefix.to_vec(), end);
490 }
491 Some(_) => {
492 end.pop();
493 }
494 None => return (prefix.to_vec(), Vec::new()),
495 }
496 }
497}
498
499fn log_key(seq: u64) -> Vec<u8> {
500 let mut k = LOG_PREFIX.to_vec();
501 k.extend_from_slice(format!("{seq:020}").as_bytes());
502 k
503}
504
505/// Terminal status of a commit key, as stored durably.
506#[derive(Debug, Clone, Copy, PartialEq, Eq)]
507pub enum OutcomeKind {
508 /// The commit succeeded at the given commit index.
509 Committed(u64),
510 /// The commit was terminally aborted and must not be retried under the
511 /// same commit key.
512 Aborted,
513}
514
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516enum OutcomeStatus {
517 Committed,
518 Aborted,
519}
520
521/// Result of a commit attempt: either the durable commit index plus the
522/// caller payload, or a conflict reason.
523#[derive(Debug)]
524pub enum CommitOutcome<Payload> {
525 /// The commit was published at `commit_index` with the caller's `payload`.
526 Committed {
527 /// Durable commit index assigned to this commit.
528 commit_index: u64,
529 /// Caller-supplied response payload persisted with the outcome.
530 payload: Payload,
531 },
532 /// The commit was rejected; the string explains the conflict.
533 Conflicted(String),
534}
535
536/// Deterministic state-machine expansion callback: derives every canonical
537/// and derived key implied by one committed batch (document 07).
538pub type ExpandFn = Box<dyn Fn(u64, &CommitBatch, &mut Txn) -> Result<(), String> + Send + Sync>;
539
540/// Accumulated state of one in-flight cross-service transaction. The buffer is
541/// the transaction's uncommitted write overlay (read-buffer-first like `Txn`);
542/// `deps` records the public reads performed inside the transaction;
543/// `mutations`/`preconditions` replay the writes atomically at commit.
544#[derive(Clone)]
545pub struct TxAccumulator {
546 /// Commit index the transaction's snapshot reads are based on.
547 pub base_ci: u64,
548 /// Tenant that owns the transaction.
549 pub tenant: TenantId,
550 /// Uncommitted write overlay, keyed by storage key.
551 pub buffer: BTreeMap<Vec<u8>, Option<Vec<u8>>>,
552 /// Public read dependencies recorded inside the transaction.
553 pub deps: DependencySet,
554 /// Mutations accumulated across statements, replayed at commit.
555 pub mutations: Vec<rushdb_types::command::Mutation>,
556 /// Preconditions accumulated across statements, checked at commit.
557 pub preconditions: Vec<rushdb_types::command::Precondition>,
558 /// Pinned snapshot all transaction reads resolve at. Cross-request
559 /// coordinators set this at BEGIN so statements and expansion observe the
560 /// transaction's snapshot rather than current committed state. `None`
561 /// keeps current-state reads for embedded/short-lived callers.
562 pub base_view: Option<std::sync::Arc<ReadView>>,
563}
564
565impl TxAccumulator {
566 /// Create an empty accumulator for `tenant` whose snapshot is `base_ci`.
567 #[must_use]
568 pub fn new(tenant: TenantId, base_ci: u64) -> Self {
569 TxAccumulator {
570 base_ci,
571 tenant,
572 buffer: BTreeMap::new(),
573 deps: DependencySet::default(),
574 mutations: Vec::new(),
575 preconditions: Vec::new(),
576 base_view: None,
577 }
578 }
579}
580
581/// Replication seam: when installed on a `Store`, local commits build the
582/// canonical `CommitBatch` and route it through the leader. The leader
583/// replicates to the quorum and every node (leader included) applies it
584/// through `Store::apply_replicated_batch`, which returns after the batch is
585/// durably applied locally. Serialized `payload` is stored with the outcome so
586/// idempotent retries can reproduce the caller's response.
587pub trait Replicator: Send + Sync {
588 /// Replicate `batch` with its serialized `payload` to the quorum.
589 ///
590 /// # Errors
591 /// Returns a human-readable reason if replication fails.
592 fn replicate(&self, batch: &CommitBatch, payload: &[u8]) -> Result<(), String>;
593
594 /// Highest commit index durably acknowledged by the replication quorum.
595 ///
596 /// Implementations that cannot expose a watermark return `None`; the
597 /// retention worker then leaves the replica consumer unregistered rather
598 /// than guessing that a lagging replica is caught up.
599 fn acknowledged_commit_index(&self) -> Option<u64> {
600 None
601 }
602}
603
604/// One superseded value of a key, covering the half-open range [from,to).
605#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
606pub struct VersionEntry {
607 /// Commit index at which this version became current.
608 pub from_ci: u64,
609 /// Commit index that superseded this version; `0` means still current.
610 pub to_ci: u64,
611 /// Stored value; `None` represents a deletion.
612 pub value: Option<Vec<u8>>,
613}
614
615/// Ordered key/value store shared by every node: admission, deterministic
616/// expansion, group-commit publication, multi-version reads, and retention.
617pub struct Store {
618 health: Arc<health::StoreHealth>,
619 resources: resources::ByteBudget,
620 transaction_resources: resources::ByteBudget,
621 /// Persistent shared budget for retained logical caches.
622 logical_cache_resources: Option<resources::ByteBudget>,
623 /// Shared reservation pool for in-flight query/adapter phases.
624 phase_resources: Option<resources::ByteBudget>,
625 /// Per-adapter attribution children of the two budgets above.
626 adapter_budgets: adapter_budgets::AdapterBudgets,
627 planner_stats: Arc<planner_stats::PlannerStats>,
628 owner_sets: Arc<owner_sets::OwnerSets>,
629 resource_config: resources::StorageResources,
630 data: Arc<RwLock<BTreeMap<Vec<u8>, Vec<u8>>>>,
631 /// creation commit index per live key (multi-version reads).
632 created: Arc<RwLock<BTreeMap<Vec<u8>, u64>>>,
633 /// superseded values per key for time-travel reads.
634 history: Arc<RwLock<BTreeMap<Vec<u8>, Vec<VersionEntry>>>>,
635 /// lowest commit index that must remain readable (GC watermark).
636 gc_watermark: AtomicU64,
637 /// Logical retention leases (held under `publish_lock` so a successful
638 /// view cannot race GC). Each entry maps a pinned CI to its [`ReadPin`]
639 /// (count, last-lease-ms, generation) so an abandoned reader can be
640 /// expired by age (P06 safe cancellation-before-release).
641 read_pins: Arc<Mutex<BTreeMap<u64, ReadPin>>>,
642 /// Cancellation handles corresponding to each retained read lease. GC
643 /// cancels a lease before removing its retention pin, so an expired view
644 /// cannot continue reading after history is reclaimed.
645 read_cancellations: Arc<Mutex<BTreeMap<u64, Vec<ReadCancellation>>>>,
646 /// Transitional single logical writer. Planning and expansion see the
647 /// fully published predecessor, including global metadata shared by tenants.
648 /// Also owns the sequencer's pending overlay (`SequencerState`).
649 admission_lock: Mutex<SequencerState>,
650 /// A failed fsync can have an uncertain durable outcome. Fence all later
651 /// writes until reopen; never retry publication under another CI in-process.
652 publisher_failure: Mutex<Option<StorageError>>,
653 #[cfg(test)]
654 fail_next_persist: std::sync::atomic::AtomicBool,
655 /// Per-tenant outer admission shards (replication planning order).
656 shards: Vec<Mutex<()>>,
657 /// Serializes durable persist + in-memory publish in commit-index order.
658 publish_lock: Mutex<()>,
659 /// FIFO gate in front of `admission_lock` (see `fair_gate`).
660 admission_queue: fair_gate::FairGate,
661 /// Write sets of recently published commits for optimistic conflict
662 /// validation (in memory only; see `conflict_window`).
663 conflicts: Mutex<conflict_window::ConflictWindow>,
664 /// Fully-admitted op-sets awaiting batched publication, keyed by their
665 /// reserved commit index (drained from `published+1`).
666 pending: Mutex<BTreeMap<u64, PendingCommit>>,
667 pending_cv: Condvar,
668 /// Commit-index reservation counter (1-based). Reservation happens at
669 /// admission; only successful commits consume a slot so the log stays
670 /// dense and replay stays position-indexed.
671 next_commit_index: AtomicU64,
672 /// Last durably persisted + applied index. `committed_version()` reads
673 /// this watermark (a reservation may run ahead of publication).
674 published: AtomicU64,
675 expander: RwLock<Option<Arc<ExpandFn>>>,
676 /// Called with each durable commit's latency in seconds (metrics hook).
677 commit_observer: RwLock<Option<Arc<CommitObserver>>>,
678 /// Apply-time deriver; `None` applies base ops unchanged.
679 deriver: RwLock<Option<Arc<dyn Deriver>>>,
680 /// Bounded tenant-directory resolution cache.
681 tenant_cache: Mutex<tenant_dir::TenantCache>,
682 /// Set when WAL recovery applied records with no deriver installed, so a
683 /// later `set_deriver` fences the store instead of serving missing
684 /// derived rows.
685 underived_replay: std::sync::atomic::AtomicBool,
686 /// One durable backend per physical shard. `shard_count == 1` (default)
687 /// keeps a single element, preserving today's single-file layout. A future
688 /// per-shard commit-index split (W4 phase 2b) routes each tenant's commits
689 /// to its shard's backend for parallel writers.
690 durable: RwLock<Box<[Option<Arc<dyn StorageBackend>>]>>,
691 /// Log-first segmented WAL: when present, the durable truth — a
692 /// commit acks after its op-set is appended+fsynced here, and redb is a
693 /// checkpointed materialization replayed from it. `None` for in-memory.
694 log: Mutex<Option<log_store::LogStore>>,
695 /// Optional replication seam (`OpenRaft` leader): when set, local commits
696 /// route through the leader instead of publishing locally; every node
697 /// (including this one) applies the result via `apply_replicated_batch`.
698 replicator: RwLock<Option<Arc<dyn Replicator>>>,
699 /// Resumable GC cursor: the last history key the background worker
700 /// processed. `None` means no sweep is in progress.
701 gc_cursor: Mutex<Option<Vec<u8>>>,
702 /// Primary durable file path (shard 0), set by `attach_redb`. Used only
703 /// for operator-facing size metrics.
704 data_path: Mutex<Option<std::path::PathBuf>>,
705 /// Copy-on-write state of the data files, recorded at open.
706 copy_on_write: Mutex<Option<CopyOnWrite>>,
707 /// Set at shutdown so long maintenance scans stop promptly and the store
708 /// closes cleanly instead of waiting for a full keyspace scan.
709 maintenance_abort: Arc<AtomicBool>,
710 /// Operator counters for retention/compaction sweeps.
711 log_rows_pruned: AtomicU64,
712 receipts_swept: AtomicU64,
713 /// Clock of retry receipts (unix ms); injectable so tests can age them.
714 receipt_clock: RwLock<Arc<dyn Fn() -> u64 + Send + Sync>>,
715 compactions: AtomicU64,
716 /// Number of group-commit redb write transactions persisted (one fsync
717 /// each under `Local`); the coalescing metric for the linger knob.
718 group_commits: AtomicU64,
719 admission_waits: AtomicU64,
720 admission_wait_ms: AtomicU64,
721 checkpoints: AtomicU64,
722 checkpoint_wait_ms: AtomicU64,
723 /// Wall-clock ms of the last published commit; compaction quiescence gate.
724 last_commit_ms: AtomicU64,
725 /// File bytes immediately after the last compaction; growth-ratio gate.
726 last_compacted_file_bytes: AtomicU64,
727 /// Keys erased by purges (tenant purge) since the last compaction: a
728 /// purge frees pages without growing the file, so it triggers idle
729 /// compaction on its own.
730 purged_since_compaction: AtomicU64,
731 /// Bytes appended to the WAL, cumulative (see `StorageStatistics`).
732 wal_bytes_appended: AtomicU64,
733 /// Encoded op bytes handed to the backends, cumulative.
734 applied_bytes: AtomicU64,
735 /// Whether a physical compaction holds the database: every read and
736 /// commit waits on it, so readiness reports it.
737 compacting: AtomicBool,
738}
739
740impl Default for Store {
741 fn default() -> Self {
742 Self::new_in_memory()
743 }
744}
745
746impl Store {
747 /// Create an in-memory store with default resource budgets.
748 ///
749 /// # Panics
750 /// Panics if the default storage resources are invalid, which is a
751 /// compile-time constant and cannot happen at runtime.
752 #[must_use]
753 pub fn new_in_memory() -> Self {
754 Self::new_with_resources(resources::StorageResources::default())
755 .expect("valid default storage resources")
756 }
757
758 /// Create an in-memory store with an explicit resource configuration.
759 ///
760 /// # Errors
761 /// Returns [`StorageError::InvalidConfiguration`] when `config` fails
762 /// validation, or [`StorageError::ResourceExhausted`] when a byte budget
763 /// cannot be created.
764 #[expect(
765 clippy::too_many_lines,
766 reason = "the store's constructor initializes every counter and lock in one place"
767 )]
768 pub fn new_with_resources(config: resources::StorageResources) -> Result<Self, StorageError> {
769 config
770 .validate()
771 .map_err(|e| StorageError::InvalidConfiguration(e.to_string()))?;
772 let resources = resources::ByteBudget::new("store", config.managed_limit_bytes)
773 .map_err(|e| StorageError::ResourceExhausted(e.to_string()))?;
774 let transaction_resources = resources
775 .child("transaction-staging", config.transaction_pool_bytes)
776 .map_err(|e| StorageError::InvalidConfiguration(e.to_string()))?;
777 // A zero-sized pool is disabled; otherwise it is capped by the root.
778 let optional_child = |name: &'static str, bytes: usize| {
779 (bytes > 0)
780 .then(|| resources.child(name, bytes.min(config.managed_limit_bytes)))
781 .transpose()
782 .map_err(|e| StorageError::InvalidConfiguration(e.to_string()))
783 };
784 let phase_resources = optional_child("query-phases", config.phase_budget_bytes)?;
785 let logical_cache_resources = optional_child("logical-cache", config.logical_cache_bytes)?;
786 let adapter_budgets = adapter_budgets::AdapterBudgets::new(
787 optional_child("vector-cache", config.vector_cache_bytes)?,
788 phase_resources.as_ref(),
789 )?;
790 let owner_sets = Arc::new(owner_sets::OwnerSets::new(
791 logical_cache_resources.as_ref(),
792 )?);
793 let planner_stats = Arc::new(planner_stats::PlannerStats::new(
794 &config,
795 logical_cache_resources.as_ref(),
796 Arc::new(std::time::Instant::now),
797 )?);
798 // The in-memory store is a real `StorageBackend`. `Store` keeps
799 // shared handles to the same maps for diagnostics/replay; the backend
800 // owns the read view and the write path.
801 let health = Arc::new(health::StoreHealth::default());
802 let maintenance_abort = Arc::new(AtomicBool::new(false));
803 let maps = memory_backend::MemoryMaps::new();
804 let memory_backends: Box<[Option<Arc<dyn StorageBackend>>]> = (0..config.shard_count)
805 .map(|_| {
806 Some(Arc::new(memory_backend::MemoryBackend::new(
807 Arc::clone(&health),
808 memory_backend::MemoryMaps {
809 data: Arc::clone(&maps.data),
810 created: Arc::clone(&maps.created),
811 history: Arc::clone(&maps.history),
812 },
813 Arc::clone(&maintenance_abort),
814 )) as Arc<dyn StorageBackend>)
815 })
816 .collect();
817 let conflicts = conflict_window::ConflictWindow::charged_to(&resources);
818 Ok(Store {
819 resources,
820 transaction_resources,
821 logical_cache_resources,
822 phase_resources,
823 adapter_budgets,
824 planner_stats,
825 owner_sets,
826 resource_config: config,
827 data: Arc::clone(&maps.data),
828 created: Arc::clone(&maps.created),
829 history: Arc::clone(&maps.history),
830 gc_watermark: AtomicU64::new(0),
831 read_pins: Arc::new(Mutex::new(BTreeMap::new())),
832 read_cancellations: Arc::new(Mutex::new(BTreeMap::new())),
833 admission_lock: Mutex::new(SequencerState::default()),
834 health,
835 publisher_failure: Mutex::new(None),
836 #[cfg(test)]
837 fail_next_persist: std::sync::atomic::AtomicBool::new(false),
838 shards: (0..N_SHARDS).map(|_| Mutex::new(())).collect(),
839 publish_lock: Mutex::new(()),
840 admission_queue: fair_gate::FairGate::default(),
841 conflicts: Mutex::new(conflicts),
842 pending: Mutex::new(BTreeMap::new()),
843 pending_cv: Condvar::new(),
844 next_commit_index: AtomicU64::new(1),
845 published: AtomicU64::new(0),
846 expander: RwLock::new(None),
847 commit_observer: RwLock::new(None),
848 deriver: RwLock::new(None),
849 tenant_cache: Mutex::new(tenant_dir::TenantCache::default()),
850 underived_replay: std::sync::atomic::AtomicBool::new(false),
851 durable: RwLock::new(memory_backends),
852 log: Mutex::new(None),
853 replicator: RwLock::new(None),
854 gc_cursor: Mutex::new(None),
855 data_path: Mutex::new(None),
856 copy_on_write: Mutex::new(None),
857 maintenance_abort,
858 log_rows_pruned: AtomicU64::new(0),
859 receipts_swept: AtomicU64::new(0),
860 receipt_clock: RwLock::new(Arc::new(wal::now_ms)),
861 compactions: AtomicU64::new(0),
862 group_commits: AtomicU64::new(0),
863 admission_waits: AtomicU64::new(0),
864 admission_wait_ms: AtomicU64::new(0),
865 checkpoints: AtomicU64::new(0),
866 checkpoint_wait_ms: AtomicU64::new(0),
867 last_commit_ms: AtomicU64::new(0),
868 last_compacted_file_bytes: AtomicU64::new(0),
869 purged_since_compaction: AtomicU64::new(0),
870 wal_bytes_appended: AtomicU64::new(0),
871 applied_bytes: AtomicU64::new(0),
872 compacting: AtomicBool::new(false),
873 })
874 }
875
876 /// Whether a physical compaction currently holds the database. Reads
877 /// and commits wait for it; `/ready` reports it so an orchestrator does
878 /// not route traffic to a node that cannot answer.
879 #[must_use]
880 pub fn compacting(&self) -> bool {
881 self.compacting.load(Ordering::Relaxed)
882 }
883
884 /// Mark a physical compaction as holding (or releasing) the database.
885 /// Set by compaction itself; public for tests of what observes it.
886 #[doc(hidden)]
887 pub fn mark_compacting(&self, on: bool) {
888 self.compacting.store(on, Ordering::Relaxed);
889 }
890
891 /// `(WAL bytes appended, op bytes applied)` since the store opened: the
892 /// two write streams a commit produces.
893 #[must_use]
894 pub fn write_bytes(&self) -> (u64, u64) {
895 (
896 self.wal_bytes_appended.load(Ordering::Relaxed),
897 self.applied_bytes.load(Ordering::Relaxed),
898 )
899 }
900
901 /// The store's managed-byte budget.
902 pub fn resource_budget(&self) -> resources::ByteBudget {
903 self.resources.clone()
904 }
905
906 /// The configuration this store was opened with.
907 pub fn resource_config(&self) -> resources::StorageResources {
908 self.resource_config
909 }
910
911 /// The byte budget charged for transaction staging.
912 pub fn transaction_budget(&self) -> resources::ByteBudget {
913 self.transaction_resources.clone()
914 }
915
916 /// Approximate bytes resident in the writer's current-value and history
917 /// maps. This is the transitional RAM mirror P04 removes; the baseline
918 /// harness records it so the "no full RAM mirror" milestone is
919 /// measurable and comparable across revisions.
920 ///
921 /// Lock poisoning fences the store until reopen.
922 pub fn approximate_map_bytes(&self) -> (u64, u64) {
923 let data = self.health.cleanup(self.data.read());
924 let current: u64 = data.iter().map(|(k, v)| (k.len() + v.len()) as u64).sum();
925 drop(data);
926 let history = self.health.cleanup(self.history.read());
927 let hist: u64 = history
928 .iter()
929 .map(|(k, v)| {
930 k.len() as u64
931 + v.iter()
932 .map(|e| e.value.as_ref().map_or(0, std::vec::Vec::len) as u64)
933 .sum::<u64>()
934 })
935 .sum();
936 (current, hist)
937 }
938
939 /// Shard index for a tenant; commits without a tenant use shard 0.
940 #[expect(
941 clippy::cast_possible_truncation,
942 reason = "the hash is immediately reduced modulo N_SHARDS, so truncating its high bits cannot change routing correctness"
943 )]
944 fn shard_for(tenant: Option<&TenantId>) -> usize {
945 match tenant {
946 Some(t) => {
947 let mut h = 2_166_136_261u64;
948 for b in &t.to_be_bytes() {
949 h ^= u64::from(*b);
950 h = h.wrapping_mul(16_777_619);
951 }
952 (h as usize) % N_SHARDS
953 }
954 None => 0,
955 }
956 }
957
958 /// Open a crash-safe redb-backed store. The materialized keyspace and
959 /// MVCC metadata are restored before the store is returned.
960 ///
961 /// # Errors
962 /// Returns [`StorageError`] if the resource configuration is invalid, the
963 /// backend cannot be opened, or its durable watermark cannot be read.
964 pub fn open(path: &std::path::Path) -> Result<Self, StorageError> {
965 Self::open_with_resources(path, resources::StorageResources::default())
966 }
967
968 /// Open a redb-backed store with an explicit resource configuration.
969 ///
970 /// # Errors
971 /// Returns [`StorageError`] if `config` is invalid, the shard backends
972 /// cannot be opened, or their durable watermarks cannot be read.
973 pub fn open_with_resources(
974 path: &std::path::Path,
975 config: resources::StorageResources,
976 ) -> Result<Self, StorageError> {
977 Self::open_with_deriver(path, config, None)
978 }
979
980 /// Open a redb-backed store with the apply-time deriver installed
981 /// *before* WAL recovery, so replayed commits derive the same rows live
982 /// publication would have.
983 ///
984 /// # Errors
985 /// Returns [`StorageError`] if `config` is invalid, the shard backends
986 /// cannot be opened, their watermarks cannot be read, or replay fails.
987 pub fn open_with_deriver(
988 path: &std::path::Path,
989 config: resources::StorageResources,
990 deriver: Option<Arc<dyn Deriver>>,
991 ) -> Result<Self, StorageError> {
992 let store = Self::new_with_resources(config)?;
993 if let Some(deriver) = deriver {
994 store.set_deriver(deriver);
995 }
996 store.attach_redb(path)?;
997 Ok(store)
998 }
999
1000 fn attach_redb(&self, path: &std::path::Path) -> Result<(), StorageError> {
1001 self.check_health()?;
1002 let shard_count = self.resource_config.shard_count;
1003 let layout_info =
1004 layout::validate_layout(path, shard_count).map_err(StorageError::Durability)?;
1005 // Before any file is created, so the shard files and the WAL inherit
1006 // it (SCALE-2: checkpoint fsyncs on copy-on-write stall every tenant).
1007 if let Some(parent) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
1008 std::fs::create_dir_all(parent)
1009 .map_err(|error| StorageError::Durability(error.to_string()))?;
1010 }
1011 copy_on_write::opt_out(path);
1012 let cache_pool = self
1013 .resources
1014 .child("redb-cache", self.resource_config.page_cache_bytes)
1015 .map_err(|e| StorageError::ResourceExhausted(e.to_string()))?;
1016 let mut backends: Vec<Option<Arc<dyn StorageBackend>>> = Vec::with_capacity(shard_count);
1017 for shard in 0..shard_count {
1018 // Shard 0 keeps the historical single-file name; shard N uses a
1019 // suffixed file. With shard_count == 1 the layout is unchanged.
1020 let shard_path = if shard == 0 {
1021 path.to_path_buf()
1022 } else {
1023 let mut os = path.as_os_str().to_os_string();
1024 os.push(format!("-{shard}"));
1025 std::path::PathBuf::from(os)
1026 };
1027 // Split the configured page cache across physical shards.
1028 let shard_cache_bytes = self.resource_config.page_cache_bytes / shard_count;
1029 let cache = cache_pool
1030 .child("shard", shard_cache_bytes)
1031 .and_then(|pool| pool.try_reserve(shard_cache_bytes))
1032 .map_err(|e| StorageError::ResourceExhausted(e.to_string()))?;
1033 let backend =
1034 backend::open_backend(&shard_path, cache).map_err(StorageError::Durability)?;
1035 backends.push(Some(backend));
1036 }
1037 // Each shard file holds its tenants' data plus the global
1038 // log/outcome/last-applied rows for commits routed to it. The durable
1039 // LSN and GC floor are read for the watermark; the data/created/history
1040 // maps are NOT materialized: a redb-backed store reads natively
1041 // and stays memory-bounded regardless of dataset size.
1042 let mut gc_watermark = 0u64;
1043 let mut commit_index = 0u64;
1044 let mut materialization: Vec<u64> = Vec::with_capacity(shard_count);
1045 for backend in backends.iter().flatten() {
1046 let (lsn, floor) = backend.load_watermark().map_err(StorageError::Durability)?;
1047 materialization.push(lsn);
1048 gc_watermark = gc_watermark.max(floor);
1049 commit_index = commit_index.max(lsn);
1050 }
1051 // Log-first: open the segmented WAL (the durable truth) and replay any
1052 // tail beyond each shard's materialization.
1053 let (log, recovered) = self.open_log_and_replay(path, &backends, &materialization)?;
1054 commit_index = commit_index.max(recovered);
1055 self.gc_watermark.store(gc_watermark, Ordering::SeqCst);
1056 self.next_commit_index
1057 .store(commit_index + 1, Ordering::SeqCst);
1058 self.published.store(commit_index, Ordering::SeqCst);
1059 *self.health.guard(self.log.lock())? = Some(log);
1060 *self.health.guard(self.durable.write())? = backends.into_boxed_slice();
1061 *self.health.guard(self.data_path.lock())? = Some(path.to_path_buf());
1062 *self.health.guard(self.copy_on_write.lock())? = Some(copy_on_write::opt_out(path));
1063 // Idle compaction measures growth from the file as opened, not from
1064 // zero: with a zero baseline every boot compacted the whole file
1065 // under the publish lock (27 min at 10M records, SCALE-1).
1066 self.last_compacted_file_bytes
1067 .store(self.storage_file_bytes().unwrap_or(0), Ordering::Relaxed);
1068 if layout_info.is_none() {
1069 layout::create_manifest(path, shard_count).map_err(StorageError::Durability)?;
1070 }
1071 Ok(())
1072 }
1073
1074 /// The durable backend for a physical shard, if this store has one.
1075 fn backend(&self, shard: usize) -> Option<Arc<dyn StorageBackend>> {
1076 self.health
1077 .cleanup(self.durable.read())
1078 .get(shard)
1079 .and_then(std::clone::Clone::clone)
1080 }
1081
1082 /// Shard 0 backend (the global single-file layout).
1083 fn primary_backend(&self) -> Option<Arc<dyn StorageBackend>> {
1084 self.backend(0)
1085 }
1086
1087 /// The planner-statistics cache: idle eviction and metrics.
1088 #[must_use]
1089 pub fn planner_stats(&self) -> &PlannerStats {
1090 &self.planner_stats
1091 }
1092
1093 /// The persistent shared byte budget for logical caches, or `None`
1094 /// when `resources.logical-cache-bytes` is 0. Every retained cache/cursor
1095 /// reservation from this store contends on this same child.
1096 #[must_use]
1097 pub fn cache_budget(&self) -> Option<resources::ByteBudget> {
1098 self.logical_cache_resources.clone()
1099 }
1100
1101 /// Shared byte budget for active query/adapter phases: one persistent
1102 /// child of the resource tree, so concurrent requests share one cap.
1103 #[must_use]
1104 pub fn phase_budget(&self) -> Option<resources::ByteBudget> {
1105 self.phase_resources.clone()
1106 }
1107
1108 /// All open shard backends, in shard order.
1109 fn all_backends(&self) -> Vec<Arc<dyn StorageBackend>> {
1110 self.health
1111 .cleanup(self.durable.read())
1112 .iter()
1113 .flatten()
1114 .cloned()
1115 .collect()
1116 }
1117
1118 /// Run `op` on every shard backend and collect the results in shard order,
1119 /// with the first error (in shard order) winning. Shards are independent
1120 /// files, so with more than one shard the work runs concurrently — this is
1121 /// what makes multi-shard GC/compaction scale instead of serializing.
1122 fn for_each_backend_parallel<T, F>(&self, op: F) -> Result<Vec<T>, StorageError>
1123 where
1124 T: Send,
1125 F: Fn(&dyn StorageBackend) -> Result<T, String> + Sync,
1126 {
1127 self.check_health()?;
1128 let backends = self.all_backends();
1129 if backends.len() <= 1 {
1130 let mut out = Vec::with_capacity(backends.len());
1131 for backend in &backends {
1132 out.push(op(backend.as_ref()).map_err(StorageError::Durability)?);
1133 }
1134 return Ok(out);
1135 }
1136 std::thread::scope(|scope| {
1137 let handles: Vec<_> = backends
1138 .iter()
1139 .map(|backend| scope.spawn(|| op(backend.as_ref())))
1140 .collect();
1141 let mut out = Vec::with_capacity(handles.len());
1142 let mut first_err: Option<String> = None;
1143 for handle in handles {
1144 match handle.join() {
1145 Ok(Ok(value)) => out.push(value),
1146 Ok(Err(error)) => {
1147 if first_err.is_none() {
1148 first_err = Some(error);
1149 }
1150 }
1151 Err(_) => {
1152 if first_err.is_none() {
1153 first_err = Some("backend worker panicked".to_string());
1154 }
1155 }
1156 }
1157 }
1158 match first_err {
1159 Some(error) => Err(StorageError::Durability(error)),
1160 None => Ok(out),
1161 }
1162 })
1163 }
1164
1165 /// Acquire the admission lock, honoring the configured acquisition timeout.
1166 /// A bounded wait turns unbounded writer queueing into a retryable
1167 /// `resource exhausted` instead of an unbounded stall (config #4).
1168 fn acquire_admission(&self) -> Result<std::sync::MutexGuard<'_, SequencerState>, StorageError> {
1169 self.check_health()?;
1170 let timeout_ms = self.resource_config().lock_acquisition_timeout_ms;
1171 if timeout_ms == 0 {
1172 // FIFO: the turn is held until the mutex is acquired, so waiting
1173 // writers acquire admission in arrival order (no starvation).
1174 let _turn = self.admission_queue.enter();
1175 return self.health.guard(self.admission_lock.lock());
1176 }
1177 let started = std::time::Instant::now();
1178 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
1179 loop {
1180 match self.admission_lock.try_lock() {
1181 Ok(guard) => {
1182 let waited = started.elapsed();
1183 if waited > std::time::Duration::ZERO {
1184 self.admission_waits.fetch_add(1, Ordering::Relaxed);
1185 self.admission_wait_ms.fetch_add(
1186 u64::try_from(waited.as_millis()).unwrap_or(u64::MAX),
1187 Ordering::Relaxed,
1188 );
1189 }
1190 return Ok(guard);
1191 }
1192 Err(std::sync::TryLockError::Poisoned(poisoned)) => {
1193 drop(poisoned);
1194 self.health.fence();
1195 return Err(StorageError::Unhealthy);
1196 }
1197 Err(std::sync::TryLockError::WouldBlock) => {
1198 if std::time::Instant::now() >= deadline {
1199 let waited = started.elapsed();
1200 self.admission_waits.fetch_add(1, Ordering::Relaxed);
1201 self.admission_wait_ms.fetch_add(
1202 u64::try_from(waited.as_millis()).unwrap_or(u64::MAX),
1203 Ordering::Relaxed,
1204 );
1205 return Err(StorageError::ResourceExhausted(
1206 "admission lock acquisition timed out".into(),
1207 ));
1208 }
1209 std::thread::sleep(std::time::Duration::from_millis(1));
1210 }
1211 }
1212 }
1213 }
1214
1215 /// Drop the in-flight prefix the published base now subsumes. Called at the
1216 /// top of every writer's admission critical section. It reads `published`
1217 /// once; if the publisher advances in between, the overlay just carries an
1218 /// already-published op redundantly until the next trim, which is harmless
1219 /// (the base holds the same value).
1220 fn trim_sequencer(&self, state: &mut SequencerState) {
1221 let published = self.published.load(Ordering::SeqCst);
1222 state.buffer.retain(|_, (ci, _)| *ci > published);
1223 }
1224
1225 /// Assemble the pending overlay (current committed base + in-flight writes)
1226 /// used to expand a command without waiting for its predecessors to
1227 /// publish. The base is the live committed view, opened here and dropped
1228 /// with the overlay, so no read pin outlives the commit. The caller must
1229 /// hold the admission guard that owns `state`.
1230 fn sequencer_overlay(&self, state: &SequencerState) -> Result<Arc<OverlayView>, StorageError> {
1231 let base = Arc::new(self.read_view()?);
1232 let mut buffer: BTreeMap<Vec<u8>, Option<Vec<u8>>> = BTreeMap::new();
1233 for (key, (_, value)) in &state.buffer {
1234 buffer.insert(key.clone(), value.clone());
1235 }
1236 Ok(Arc::new(OverlayView::with_buffer(base, buffer)))
1237 }
1238
1239 fn check_health(&self) -> Result<(), StorageError> {
1240 if self.data.is_poisoned()
1241 || self.created.is_poisoned()
1242 || self.history.is_poisoned()
1243 || self.publish_lock.is_poisoned()
1244 || self.admission_lock.is_poisoned()
1245 || self.pending.is_poisoned()
1246 || self.publisher_failure.is_poisoned()
1247 || self.read_pins.is_poisoned()
1248 || self.read_cancellations.is_poisoned()
1249 || self.expander.is_poisoned()
1250 || self.durable.is_poisoned()
1251 || self.log.is_poisoned()
1252 || self.replicator.is_poisoned()
1253 || self.gc_cursor.is_poisoned()
1254 || self.data_path.is_poisoned()
1255 || self.shards.iter().any(Mutex::is_poisoned)
1256 {
1257 self.health.fence();
1258 }
1259 self.health.check()
1260 }
1261
1262 fn check_publisher(&self) -> Result<(), StorageError> {
1263 self.check_health()?;
1264 match self.health.guard(self.publisher_failure.lock())?.clone() {
1265 Some(error) => Err(error),
1266 None => Ok(()),
1267 }
1268 }
1269
1270 /// Return the terminal publisher failure, if durable publication has been
1271 /// fenced. Readiness callers use this to avoid routing traffic to a store
1272 /// whose later commits cannot become durable.
1273 ///
1274 /// Lock poisoning fences the store until reopen.
1275 #[must_use]
1276 pub fn publisher_failure(&self) -> Option<String> {
1277 if let Err(error) = self.check_health() {
1278 return Some(error.to_string());
1279 }
1280 self.health
1281 .cleanup(self.publisher_failure.lock())
1282 .as_ref()
1283 .map(ToString::to_string)
1284 }
1285
1286 fn native_transaction_view(&self) -> Result<Option<redb_backend::MergedViews>, StorageError> {
1287 self.check_health()?;
1288 let backends = self.all_backends();
1289 if backends.is_empty() {
1290 return Ok(None);
1291 }
1292 let mut views: Vec<Box<dyn crate::backend::StorageReadView>> =
1293 Vec::with_capacity(backends.len());
1294 for backend in &backends {
1295 views.push(backend.read_view()?);
1296 }
1297 Ok(Some(redb_backend::MergedViews::new(views)))
1298 }
1299
1300 /// Install the deterministic expander used to derive canonical and
1301 /// derived keys for every committed batch.
1302 ///
1303 /// Lock poisoning fences the store until reopen.
1304 pub fn set_expander(&self, expander: Arc<ExpandFn>) {
1305 *self.health.cleanup(self.expander.write()) = Some(expander);
1306 }
1307
1308 /// Whether this store persists to a durable backend. A construction
1309 /// property (was a file opened?), not a property of the map contents.
1310 #[must_use]
1311 pub fn is_durable(&self) -> bool {
1312 self.health.cleanup(self.data_path.lock()).is_some()
1313 }
1314
1315 /// Install a replication seam (e.g. an `OpenRaft` leader). When set, local
1316 /// commits route through the leader instead of publishing locally.
1317 ///
1318 /// Lock poisoning fences the store until reopen.
1319 pub fn set_replicator(&self, replicator: Option<Arc<dyn Replicator>>) {
1320 *self.health.cleanup(self.replicator.write()) = replicator;
1321 }
1322
1323 fn replicator(&self) -> Option<Arc<dyn Replicator>> {
1324 self.health.cleanup(self.replicator.read()).clone()
1325 }
1326
1327 /// Return the replication watermark when the configured replicator can
1328 /// expose one. A missing watermark is deliberately fail-closed for
1329 /// retention: history is retained until coverage is observable.
1330 #[must_use]
1331 pub fn replication_acknowledged_commit(&self) -> Option<u64> {
1332 self.replicator()
1333 .and_then(|replicator| replicator.acknowledged_commit_index())
1334 }
1335
1336 /// Last durably persisted and applied commit index.
1337 pub fn committed_version(&self) -> u64 {
1338 self.published.load(Ordering::SeqCst)
1339 }
1340
1341 /// Public idempotency probe: durable commit index for a commit key.
1342 pub fn committed_index_for(&self, commit_key: Uuid) -> Option<u64> {
1343 self.lookup_outcome(commit_key)
1344 .filter(|(st, _)| matches!(st, OutcomeStatus::Committed))
1345 .map(|(_, ci)| ci)
1346 }
1347
1348 /// Public outcome probe for transaction coordinators: distinguishes a
1349 /// durable committed index from a durable aborted outcome.
1350 pub fn outcome_for(&self, commit_key: Uuid) -> Option<OutcomeKind> {
1351 self.lookup_outcome(commit_key).map(|(st, ci)| match st {
1352 OutcomeStatus::Committed => OutcomeKind::Committed(ci),
1353 OutcomeStatus::Aborted => OutcomeKind::Aborted,
1354 })
1355 }
1356
1357 /// Durably record an aborted outcome under a commit key (used by
1358 /// rollback so a later commit attempt discovers `aborted`, never false
1359 /// success). No data is published.
1360 ///
1361 /// # Errors
1362 /// Returns [`StorageError`] if the durable outcome write fails.
1363 ///
1364 /// Lock poisoning fences the store until reopen.
1365 pub fn record_aborted_outcome(&self, commit_key: Uuid) -> Result<(), StorageError> {
1366 self.check_health()?;
1367 let _admission = self.acquire_admission()?;
1368 self.record_aborted_outcome_admitted(commit_key)
1369 }
1370
1371 fn record_aborted_outcome_admitted(&self, commit_key: Uuid) -> Result<(), StorageError> {
1372 self.check_health()?;
1373 let _guard = self.health.guard(self.publish_lock.lock())?;
1374 if self.lookup_outcome_checked(commit_key)?.is_some() {
1375 return Ok(());
1376 }
1377 // An aborted tx consumes no commit index: the terminal aborted outcome
1378 // is keyed by the commit key and does not occupy a sequence slot, so
1379 // the contiguous publisher never has to fill a hole for it.
1380 self.record_outcome_locked(commit_key, OutcomeStatus::Aborted, 0)?;
1381 Ok(())
1382 }
1383
1384 /// Fallible latest-state point probe. Multi-read consumers must instead
1385 /// hold a `ReadView`. Write admission uses this under the logical writer.
1386 ///
1387 /// # Errors
1388 /// Returns [`StorageError`] if the publisher has failed or the native
1389 /// backend read fails.
1390 ///
1391 /// Lock poisoning fences the store until reopen.
1392 pub fn try_get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StorageError> {
1393 self.check_health()?;
1394 self.check_publisher()?;
1395 let native = self
1396 .native_transaction_view()?
1397 .ok_or(StorageError::Unhealthy)?;
1398 native.get_current(key, ReadLimits::default(), ScanProjection::KeyValue)
1399 }
1400
1401 /// Fallible outcome probe that distinguishes committed from aborted and
1402 /// surfaces corrupt durable state.
1403 ///
1404 /// # Errors
1405 /// Returns [`StorageError::CorruptLog`] if the stored outcome row is
1406 /// malformed, or a backend error if the read fails.
1407 pub fn try_outcome_for(&self, commit_key: Uuid) -> Result<Option<OutcomeKind>, StorageError> {
1408 self.check_health()?;
1409 Ok(self
1410 .lookup_outcome_checked(commit_key)?
1411 .map(|(status, ci)| match status {
1412 OutcomeStatus::Committed => OutcomeKind::Committed(ci),
1413 OutcomeStatus::Aborted => OutcomeKind::Aborted,
1414 }))
1415 }
1416
1417 /// Fallible exact-width `u64` read for engine callers.
1418 ///
1419 /// # Errors
1420 /// Returns [`StorageError::CorruptLog`] when a present value is not exactly
1421 /// eight bytes, or when the durable read fails.
1422 ///
1423 /// Lock poisoning fences the store until reopen.
1424 pub fn try_get_u64(&self, k: &[u8]) -> Result<Option<u64>, StorageError> {
1425 self.check_health()?;
1426 let bytes = self
1427 .native_transaction_view()?
1428 .ok_or(StorageError::Unhealthy)?
1429 .get_current(k, ReadLimits::default(), ScanProjection::KeyValue)?;
1430 let Some(bytes) = bytes else {
1431 return Ok(None);
1432 };
1433 let raw: [u8; 8] = bytes
1434 .as_slice()
1435 .try_into()
1436 .map_err(|_| StorageError::CorruptLog)?;
1437 Ok(Some(u64::from_be_bytes(raw)))
1438 }
1439
1440 /// Fallible historical prefix scan for engine callers.
1441 ///
1442 /// # Errors
1443 /// Returns backend or read-limit failures.
1444 ///
1445 /// Lock poisoning fences the store until reopen.
1446 pub fn try_range_prefix(&self, prefix: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>, StorageError> {
1447 self.check_health()?;
1448 let view = self
1449 .native_transaction_view()?
1450 .ok_or(StorageError::Unhealthy)?;
1451 let (s, e) = prefix_range(prefix);
1452 let rows: Result<Vec<(Vec<u8>, Vec<u8>)>, StorageError> = view
1453 .current_range(&s, &e, ReadLimits::default(), ScanProjection::KeyValue)?
1454 .collect();
1455 rows
1456 }
1457
1458 /// Fallible historical point read for engine callers.
1459 ///
1460 /// # Errors
1461 /// Returns backend or read-limit failures.
1462 ///
1463 /// Lock poisoning fences the store until reopen.
1464 pub fn try_get_at(&self, k: &[u8], snap: u64) -> Result<Option<Vec<u8>>, StorageError> {
1465 self.check_health()?;
1466 self.native_transaction_view()?
1467 .ok_or(StorageError::Unhealthy)?
1468 .get_at(k, snap, ReadLimits::default(), ScanProjection::KeyValue)
1469 }
1470
1471 /// Fallible historical prefix scan for engine callers.
1472 ///
1473 /// # Errors
1474 /// Returns backend or read-limit failures.
1475 ///
1476 /// Lock poisoning fences the store until reopen.
1477 pub fn try_range_prefix_at(
1478 &self,
1479 prefix: &[u8],
1480 snap: u64,
1481 ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, StorageError> {
1482 self.check_health()?;
1483 // Read directly through the backend view at `snap` (like `try_get_at`):
1484 // this is a fallible legacy probe, not the pinned `ReadView` path. It
1485 // therefore does not require the store's published watermark to have
1486 // caught up, which raw-apply test/recovery callers rely on.
1487 let view = self
1488 .native_transaction_view()?
1489 .ok_or(StorageError::Unhealthy)?;
1490 let limits = ReadLimits::default();
1491 let (s, e) = prefix_range(prefix);
1492 let mut lower = Included(s);
1493 let upper = if e.is_empty() {
1494 Unbounded
1495 } else {
1496 std::ops::Bound::Excluded(e)
1497 };
1498 let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
1499 let mut bytes = 0usize;
1500 while let Some(key) = view.next_key(&lower, &upper, limits, true)? {
1501 if let Some(value) = view.get_at(&key, snap, limits, ScanProjection::KeyValue)? {
1502 bytes = bytes.saturating_add(key.len() + value.len());
1503 if bytes > 64 * 1024 * 1024 {
1504 return Err(StorageError::ResourceExhausted(
1505 "materialized read exceeds configured limit; use a cursor".into(),
1506 ));
1507 }
1508 out.push((key.clone(), value));
1509 }
1510 lower = std::ops::Bound::Excluded(key);
1511 }
1512 Ok(out)
1513 }
1514
1515 /// Advances retention: superseded versions covered before the watermark
1516 /// are reclaimed; versions still needed by any newer snapshot stay.
1517 ///
1518 /// # Errors
1519 /// Returns [`StorageError`] if the durable reclamation fails.
1520 ///
1521 /// Lock poisoning fences the store until reopen.
1522 pub fn retire_before(&self, watermark: u64) -> Result<usize, StorageError> {
1523 self.check_health()?;
1524 let _publisher = self.health.guard(self.publish_lock.lock())?;
1525 self.retire_before_locked(watermark)
1526 }
1527
1528 /// Bounded, resumable GC: reclaim history superseded before `watermark`
1529 /// for at most `max_keys` per shard in this call. Returns the next key to
1530 /// resume from (or `None` when every shard finished the walk). A
1531 /// background driver calls this repeatedly; it never performs one
1532 /// monolithic redb txn.
1533 ///
1534 /// # Errors
1535 /// Returns [`StorageError`] if the publisher has failed or a shard's
1536 /// durable reclamation fails.
1537 ///
1538 /// # Panics
1539 /// Panics if the read-pin mutex or history `RwLock` is poisoned by a
1540 /// previous panic.
1541 #[expect(
1542 clippy::needless_pass_by_value,
1543 reason = "public signature is a compatibility contract; the cursor is cloned per shard so taking it by value cannot change behavior"
1544 )]
1545 pub fn retire_before_batch(
1546 &self,
1547 watermark: u64,
1548 cursor: Option<Vec<u8>>,
1549 max_keys: usize,
1550 ) -> Result<Option<Vec<u8>>, StorageError> {
1551 self.check_health()?;
1552 self.check_publisher()?;
1553 // Clamp identically to the monolithic path: never reclaim a version a
1554 // pinned read may still need, never advance the floor past the
1555 // committed version, and never move the floor backwards. The clamp is
1556 // taken under `read_pins`, the same lock reads use for their floor
1557 // check and pin insert, so no read can admit a snapshot below the new
1558 // floor.
1559 let watermark = {
1560 let pins = self.health.guard(self.read_pins.lock())?;
1561 let watermark = watermark.min(self.committed_version());
1562 let watermark = pins
1563 .first_key_value()
1564 .map_or(watermark, |(ci, _)| watermark.min(*ci));
1565 let watermark = watermark.max(self.gc_watermark());
1566 // Publish the floor *while holding* `read_pins`. A reader that
1567 // acquires the lock after this sees the new floor and is rejected;
1568 // a reader that acquired it before this is already reflected in
1569 // `pins`, so the clamp cannot be below its snapshot. Storing after
1570 // unlocking would let a reader admit a below-floor snapshot that
1571 // GC is about to reclaim.
1572 self.gc_watermark
1573 .store(watermark, std::sync::atomic::Ordering::SeqCst);
1574 watermark
1575 };
1576 // Reclaim holds no read lock, so readers are never blocked behind a GC
1577 // write transaction.
1578 // A single cursor is shared across shards. Resume from the smallest
1579 // per-shard cursor so no shard skips keys, and treat the sweep as
1580 // complete only when every shard reports no further work.
1581 let mut last: Option<Vec<u8>> = None;
1582 let mut all_done = true;
1583 for backend in self.all_backends() {
1584 if let Some(k) = backend
1585 .retire_before_batch(watermark, cursor.clone(), max_keys)
1586 .map_err(StorageError::Durability)?
1587 {
1588 all_done = false;
1589 last = Some(match last {
1590 Some(prev) if prev <= k => prev,
1591 _ => k,
1592 });
1593 }
1594 }
1595 if all_done {
1596 // Full walk completed on every shard: apply the same in-memory
1597 // retention the monolithic path applies after durable reclamation.
1598 let mut hist = self.health.guard(self.history.write())?;
1599 for entries in hist.values_mut() {
1600 entries.retain(|e| e.to_ci > watermark || e.to_ci == 0);
1601 }
1602 hist.retain(|_, v| !v.is_empty());
1603 }
1604 Ok(last)
1605 }
1606
1607 /// One tick of the background GC loop: reclaim a bounded batch toward
1608 /// the current committed watermark. Returns `true` when the whole history
1609 /// table was swept this tick (a full GC pass completed and a checkpoint
1610 /// was published per shard), `false` when the sweep is mid-walk and the
1611 /// caller should tick again. Safe to call concurrently: each tick is a
1612 /// small redb write transaction.
1613 ///
1614 /// # Errors
1615 /// Returns [`StorageError`] if the publisher has failed or a shard's
1616 /// durable reclamation fails.
1617 ///
1618 /// Lock poisoning fences the store until reopen.
1619 pub fn gc_tick(&self, max_keys: usize) -> Result<bool, StorageError> {
1620 self.check_health()?;
1621 let watermark = self.committed_version();
1622 let cursor = self.health.guard(self.gc_cursor.lock())?.clone();
1623 let next = self.retire_before_batch(watermark, cursor, max_keys)?;
1624 if let Some(k) = next {
1625 *self.health.guard(self.gc_cursor.lock())? = Some(k);
1626 Ok(false)
1627 } else {
1628 // Sweep complete: reset the cursor so the next pass starts
1629 // from the beginning (new histories are reclaimed too).
1630 *self.health.guard(self.gc_cursor.lock())? = None;
1631 Ok(true)
1632 }
1633 }
1634
1635 /// [`Self::gc_tick`] in batches of `max_keys` until the history sweep
1636 /// completes or `budget` has elapsed, so reclamation keeps up with the
1637 /// write rate instead of retiring a fixed number of keys per maintenance
1638 /// tick (SOAK-1: 512 keys per 5 s fell behind ~750 rewrites/s and history
1639 /// grew without bound). Returns whether the sweep completed.
1640 ///
1641 /// # Errors
1642 /// See [`Self::gc_tick`].
1643 pub fn gc_tick_for(
1644 &self,
1645 max_keys: usize,
1646 budget: std::time::Duration,
1647 ) -> Result<bool, StorageError> {
1648 let started = std::time::Instant::now();
1649 loop {
1650 if self.gc_tick(max_keys)? {
1651 return Ok(true);
1652 }
1653 if started.elapsed() >= budget {
1654 return Ok(false);
1655 }
1656 }
1657 }
1658
1659 fn retire_before_locked(&self, watermark: u64) -> Result<usize, StorageError> {
1660 self.check_health()?;
1661 self.check_publisher()?;
1662 let watermark = {
1663 let pins = self.health.guard(self.read_pins.lock())?;
1664 let watermark = watermark.min(self.committed_version());
1665 let watermark = pins
1666 .first_key_value()
1667 .map_or(watermark, |(ci, _)| watermark.min(*ci));
1668 let watermark = watermark.max(self.gc_watermark());
1669 // Fence reads below the new floor while still holding `read_pins`
1670 // (see `retire_before_batch`). A failed durable reclamation leaves
1671 // the floor advanced — a conservative, fail-closed state — never
1672 // the reverse.
1673 self.gc_watermark
1674 .store(watermark, std::sync::atomic::Ordering::SeqCst);
1675 watermark
1676 };
1677 // Prune the in-memory maps first so the reclaimed-version count is
1678 // observable (the durable maps are empty; the backend does its own
1679 // reclamation below). `MemoryBackend::retire_before` then re-prunes the
1680 // same maps idempotently.
1681 let mut removed = 0usize;
1682 {
1683 let mut hist = self.health.guard(self.history.write())?;
1684 for entries in hist.values_mut() {
1685 let before = entries.len();
1686 entries.retain(|e| e.to_ci > watermark || e.to_ci == 0);
1687 removed += before - entries.len();
1688 }
1689 hist.retain(|_, v| !v.is_empty());
1690 }
1691 self.for_each_backend_parallel(|backend| backend.retire_before(watermark))?;
1692 Ok(removed)
1693 }
1694
1695 /// Oldest supported logical commit index, inclusive. New historical views
1696 /// below this floor fail, even if an individual key still has older bytes.
1697 pub fn gc_watermark(&self) -> u64 {
1698 self.gc_watermark.load(std::sync::atomic::Ordering::SeqCst)
1699 }
1700
1701 /// Expire retained read leases older than `max_age_ms` (P06: reader age
1702 /// limits with safe cancellation-before-release). An abandoned reader that
1703 /// never drops its view cannot block history advancement forever; the GC
1704 /// worker calls this so the floor can move. Returns the count of leases
1705 /// released (CI entries whose remaining pin count reached zero).
1706 ///
1707 /// Lock poisoning fences the store until reopen.
1708 pub fn expire_read_pins(&self, max_age_ms: u64) -> usize {
1709 if self.check_health().is_err() {
1710 return 0;
1711 }
1712 let now = wal::now_ms();
1713 let mut pins = self.health.cleanup(self.read_pins.lock());
1714 let mut cancellations = self.health.cleanup(self.read_cancellations.lock());
1715 let mut released = 0usize;
1716 pins.retain(|ci, pin| {
1717 if now.saturating_sub(pin.lease_ms) > max_age_ms {
1718 if let Some(handles) = cancellations.remove(ci) {
1719 for handle in handles {
1720 handle.cancel();
1721 }
1722 }
1723 released += 1;
1724 false
1725 } else {
1726 true
1727 }
1728 });
1729 released
1730 }
1731
1732 /// Observability: the oldest active pin (inclusive CI floor) and how
1733 /// many distinct CIs are retained. Lets operators see long-reader retention
1734 /// and confirm it releases after handles close.
1735 ///
1736 /// Lock poisoning fences the store until reopen.
1737 pub fn retention_summary(&self) -> (Option<u64>, usize) {
1738 let pins = self.health.cleanup(self.read_pins.lock());
1739 (pins.first_key_value().map(|(ci, _)| *ci), pins.len())
1740 }
1741
1742 /// Number of keys in the sequencer's assigned-but-unpublished overlay.
1743 /// Observability for the single-writer in-flight window; returns 0
1744 /// when the admission lock is poisoned rather than panicking a scrape.
1745 #[must_use]
1746 pub fn sequencer_inflight_keys(&self) -> usize {
1747 self.admission_lock
1748 .lock()
1749 .map_or(0, |state| state.buffer.len())
1750 }
1751
1752 /// Reclaim versions superseded at or before the requested watermark,
1753 /// clamped by protected read views and the committed CI. Legacy unpinned
1754 /// readers must still coordinate their own retention. Returns reclaimed
1755 /// version count; canonical current data and command logs are unchanged.
1756 ///
1757 /// # Errors
1758 /// Returns [`StorageError`] if the durable reclamation fails.
1759 pub fn gc(&self, watermark: u64) -> Result<usize, StorageError> {
1760 self.check_health()?;
1761 self.retire_before(watermark)
1762 }
1763
1764 fn record_outcome_locked(
1765 &self,
1766 commit_key: Uuid,
1767 status: OutcomeStatus,
1768 ci: u64,
1769 ) -> Result<(), StorageError> {
1770 self.check_health()?;
1771 let ops: Vec<Op> = receipt::rows(commit_key, status, ci, self.receipt_now(), None)
1772 .into_iter()
1773 .map(|(key, value)| Op::Put(key, value))
1774 .collect();
1775 // Aborted-outcome idempotency markers are local metadata, not part of
1776 // the committed log sequence (they carry no commit index), so they are
1777 // written to the materialization directly rather than the WAL.
1778 self.persist_ops_local(&ops, ci)
1779 }
1780
1781 /// Current time of the receipt clock (unix ms).
1782 fn receipt_now(&self) -> u64 {
1783 (self.health.cleanup(self.receipt_clock.read()).clone())()
1784 }
1785
1786 /// Replace the receipt clock (tests age receipts with a mock clock).
1787 #[doc(hidden)]
1788 pub fn set_receipt_clock(&self, clock: Arc<dyn Fn() -> u64 + Send + Sync>) {
1789 *self.health.cleanup(self.receipt_clock.write()) = clock;
1790 }
1791
1792 /// Stage the receipt of `commit_key` in `txn`.
1793 fn put_receipt(
1794 &self,
1795 txn: &mut Txn<'_>,
1796 commit_key: Uuid,
1797 status: OutcomeStatus,
1798 ci: u64,
1799 payload: Option<&[u8]>,
1800 ) {
1801 for (key, value) in receipt::rows(commit_key, status, ci, self.receipt_now(), payload) {
1802 txn.put(key, value);
1803 }
1804 }
1805
1806 /// The live receipt of `commit_key`: `None` when there is none or it is
1807 /// past the idempotency window (a retry then executes as a new commit).
1808 fn live_receipt(&self, commit_key: Uuid) -> Result<Option<receipt::Receipt>, StorageError> {
1809 let Some(raw) = self.try_get(&receipt::receipt_key(commit_key))? else {
1810 return Ok(None);
1811 };
1812 let receipt = receipt::decode(&raw)?;
1813 let window_ms = self
1814 .resource_config
1815 .idempotency_window_seconds
1816 .saturating_mul(1000);
1817 Ok((!receipt.expired(self.receipt_now(), window_ms)).then_some(receipt))
1818 }
1819
1820 /// Apply an op-set to the materialization without a WAL record. Used only
1821 /// for local idempotency metadata that is not part of the commit log.
1822 fn persist_ops_local(&self, ops: &[Op], commit_index: u64) -> Result<(), StorageError> {
1823 self.check_health()?;
1824 self.check_publisher()?;
1825 if let Some(backend) = self.primary_backend() {
1826 backend
1827 .apply(ops, commit_index)
1828 .map_err(StorageError::Durability)?;
1829 }
1830 Ok(())
1831 }
1832
1833 fn lookup_outcome(&self, commit_key: Uuid) -> Option<(OutcomeStatus, u64)> {
1834 self.lookup_outcome_checked(commit_key).ok().flatten()
1835 }
1836
1837 fn lookup_outcome_checked(
1838 &self,
1839 commit_key: Uuid,
1840 ) -> Result<Option<(OutcomeStatus, u64)>, StorageError> {
1841 self.check_health()?;
1842 Ok(self
1843 .live_receipt(commit_key)?
1844 .map(|receipt| (receipt.status, receipt.commit_index)))
1845 }
1846
1847 /// The stored response of a committed `commit_key` (its retry answer).
1848 fn committed_payload<P: serde::de::DeserializeOwned>(
1849 &self,
1850 commit_key: Uuid,
1851 ) -> Result<P, StorageError> {
1852 let payload = self
1853 .live_receipt(commit_key)?
1854 .and_then(|receipt| receipt.payload)
1855 .ok_or(StorageError::OutcomePayload)?;
1856 serde_json::from_slice(&payload).map_err(|_| StorageError::OutcomePayload)
1857 }
1858
1859 /// Rebuilds state by replaying the full serialized command log through
1860 /// the registered deterministic expander.
1861 ///
1862 /// # Errors
1863 /// Returns [`StorageError::CorruptLog`] for malformed input or missing
1864 /// expansion support, and [`StorageError::Builder`] when deterministic
1865 /// expansion rejects an entry. No replayed state is published on decode or
1866 /// expansion failure.
1867 ///
1868 /// # Panics
1869 /// Panics only if an internal lock is poisoned by a previous panic.
1870 pub fn try_replay(&self, log: &[Vec<u8>]) -> Result<(), StorageError> {
1871 self.check_health()?;
1872 // A replay input is a committed command log, so silently dropping a
1873 // malformed entry would renumber every later commit and produce a
1874 // state that looks valid while diverging from the durable history.
1875 // Recovery callers that intentionally trim a torn tail must do so
1876 // before invoking replay (as the WAL reader does).
1877 let entries: Vec<CommitBatch> = log
1878 .iter()
1879 .map(|raw| {
1880 rushdb_types::command_codec::decode(raw).map_err(|_| StorageError::CorruptLog)
1881 })
1882 .collect::<Result<_, _>>()?;
1883 let expander = self.health.guard(self.expander.read())?.clone();
1884 let mut prepared = Vec::with_capacity(entries.len());
1885 // Expansion of a later command may legitimately depend on canonical
1886 // state produced by an earlier command (for example, a text
1887 // watermark refers to a profile created in the preceding commit).
1888 // Build a private shadow map while preparing the complete replay so
1889 // those dependencies are visible without publishing partial state.
1890 let mut shadow = self.health.guard(self.data.read())?.clone();
1891
1892 for (i, batch) in entries.iter().enumerate() {
1893 let ci = i as u64 + 1;
1894 let Some(expand) = expander.as_ref() else {
1895 return Err(StorageError::CorruptLog);
1896 };
1897 let ops;
1898 {
1899 // Full command replay still builds the reference state, so
1900 // the base is always the live shadow map, never native.
1901 let mut txn = Txn {
1902 base: TxnBase::Live(&shadow),
1903 buffer: BTreeMap::new(),
1904 purge_keys: BTreeSet::default(),
1905 set_keys: BTreeSet::default(),
1906 deps: None,
1907 };
1908 expand(ci, batch, &mut txn).map_err(StorageError::Builder)?;
1909 self.put_receipt(
1910 &mut txn,
1911 batch.commit_key,
1912 OutcomeStatus::Committed,
1913 ci,
1914 None,
1915 );
1916 txn.set(LAST_APPLIED_KEY.to_vec(), wal::encode_last_applied(0, ci));
1917 ops = txn.drain();
1918 }
1919 // Command replay expands against a base-only shadow; fold the
1920 // derived rows in too, so an expander that reads derived counters
1921 // (DeleteFamily) sees what a live expansion would.
1922 let derived =
1923 Self::derive_ops(self.deriver().as_ref(), ci, &ops, &derive::MapRead(&shadow))?;
1924 for op in derived.as_deref().unwrap_or(&ops) {
1925 match op {
1926 Op::Put(key, value) | Op::Set(key, value) => {
1927 shadow.insert(key.clone(), value.clone());
1928 }
1929 Op::Del(key) | Op::Purge(key) => {
1930 shadow.remove(key);
1931 }
1932 }
1933 }
1934 prepared.push((ci, ops, rushdb_types::command_codec::encode(batch)));
1935 }
1936 for (ci, ops, command_payload) in prepared {
1937 // Apply through the backend: one write path for memory and
1938 // durable replay; `persist_ops` derives the same derived rows the
1939 // live publisher would, never shipped over the wire.
1940 self.persist_ops(&ops, ci, Some(&command_payload))?;
1941 self.health.guard(self.conflicts.lock())?.record(ci, &ops);
1942 }
1943 {
1944 let applied = entries.len() as u64;
1945 self.next_commit_index.store(applied + 1, Ordering::SeqCst);
1946 self.published.store(applied, Ordering::SeqCst);
1947 self.last_commit_ms.store(wal::now_ms(), Ordering::Relaxed);
1948 self.persist_ops(
1949 &[Op::Set(
1950 LAST_APPLIED_KEY.to_vec(),
1951 wal::encode_last_applied(0, applied),
1952 )],
1953 applied,
1954 None,
1955 )?;
1956 }
1957 Ok(())
1958 }
1959
1960 /// Apply a command batch already committed by `OpenRaft`. This bypasses
1961 /// admission-time planning but uses exactly the same deterministic
1962 /// expander and durable publication path as a local commit.
1963 ///
1964 /// # Errors
1965 /// Returns [`StorageError`] if the publisher has failed, the batch has a
1966 /// terminal aborted outcome, or expansion/publication fails.
1967 pub fn apply_replicated_batch(&self, batch: &CommitBatch) -> Result<u64, StorageError> {
1968 self.check_health()?;
1969 self.apply_replicated_batch_with_payload(batch, None)
1970 }
1971
1972 /// Apply a replicated batch and durably store its serialized payload with
1973 /// the outcome so idempotent retries on the leader reproduce the original
1974 /// caller response.
1975 ///
1976 /// # Errors
1977 /// Returns [`StorageError`] if the publisher has failed, no expander is
1978 /// installed, the batch has a terminal aborted outcome, or publication
1979 /// fails.
1980 ///
1981 /// # Panics
1982 /// Panics only if `batch` fails to serialize (never, for a well-formed
1983 /// `CommitBatch`). Lock poisoning fences the store until reopen.
1984 pub fn apply_replicated_batch_with_payload(
1985 &self,
1986 batch: &CommitBatch,
1987 payload: Option<&[u8]>,
1988 ) -> Result<u64, StorageError> {
1989 self.check_health()?;
1990 let _admission = self.acquire_admission()?;
1991 self.check_publisher()?;
1992 if let Some((OutcomeStatus::Committed, commit_index)) =
1993 self.lookup_outcome_checked(batch.commit_key)?
1994 {
1995 return Ok(commit_index);
1996 }
1997
1998 if matches!(
1999 self.try_outcome_for(batch.commit_key)?,
2000 Some(OutcomeKind::Aborted)
2001 ) {
2002 return Err(StorageError::Builder(
2003 "commit key has a terminal aborted outcome".into(),
2004 ));
2005 }
2006 let commit_index = self.next_commit_index.load(Ordering::SeqCst);
2007 let ops = {
2008 let guard = self.health.guard(self.data.read())?;
2009 let native = self.native_transaction_view()?;
2010 let base = TxnBase::resolve(None, None, native, Some(&guard));
2011 let mut txn = Txn {
2012 base,
2013 buffer: BTreeMap::new(),
2014 purge_keys: BTreeSet::default(),
2015 set_keys: BTreeSet::default(),
2016 deps: None,
2017 };
2018 let expander = self.health.guard(self.expander.read())?.clone();
2019 let Some(expand) = expander.as_ref() else {
2020 return Err(StorageError::CorruptLog);
2021 };
2022 expand(commit_index, batch, &mut txn).map_err(StorageError::Builder)?;
2023 self.put_receipt(
2024 &mut txn,
2025 batch.commit_key,
2026 OutcomeStatus::Committed,
2027 commit_index,
2028 payload,
2029 );
2030 txn.set(
2031 LAST_APPLIED_KEY.to_vec(),
2032 wal::encode_last_applied(0, commit_index),
2033 );
2034 txn.drain()
2035 };
2036 // Large transactions: the same ceiling `commit_sharded`
2037 // enforces before its own CAS reservation (see `commit.rs`) applies
2038 // here too, before `next_commit_index` advances — this path has its
2039 // own, separate reservation (no CAS loop; a replicated batch's index
2040 // is assigned directly from the leader's already-ordered stream).
2041 // Operationally this means a follower configured with a *smaller*
2042 // ceiling than the leader will stall (loudly, retried forever by
2043 // `replication.rs`'s follower loop) on a commit the leader already
2044 // accepted, rather than silently diverging or truncating it —
2045 // deliberately: this is a config-mismatch to fix, not a case to
2046 // paper over. Chaining itself doesn't need this check to write a
2047 // large commit correctly; it's policy consistency, not correctness.
2048 let command_payload = rushdb_types::command_codec::encode(batch);
2049 let expanded_bytes = wal::op_set_encoded_len(&ops).saturating_add(command_payload.len());
2050 if expanded_bytes > self.resource_config.max_expanded_commit_bytes {
2051 return Err(StorageError::ResourceExhausted(format!(
2052 "expanded mutation batch exceeds {} bytes (got {expanded_bytes})",
2053 self.resource_config.max_expanded_commit_bytes
2054 )));
2055 }
2056 self.next_commit_index
2057 .store(commit_index + 1, Ordering::SeqCst);
2058 let shard =
2059 resources::physical_shard_for(Some(&batch.tenant), self.resource_config.shard_count);
2060 self.enqueue_and_publish(commit_index, shard, ops, command_payload)?;
2061 Ok(commit_index)
2062 }
2063}