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