Skip to main content

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