@@ -6,15 +6,35 @@ use crate::{CommitId, RealmId};
66#[ non_exhaustive]
77#[ derive( Debug , thiserror:: Error ) ]
88pub enum PagedbError {
9+ /// A page or footer's AEAD tag did not verify against its authenticated
10+ /// bytes, and the failure could not be attributed to a more specific
11+ /// corruption reason. Raised on the segment page decrypt-retry path (all
12+ /// candidate page kinds failed) and inside the raw cipher open call.
13+ /// Treat the containing file or page as untrustworthy; a fuller
14+ /// diagnosis usually surfaces as one of the [`CorruptionDetail`]
15+ /// variants instead, so see those first when triaging.
916 #[ error( "checksum / AEAD tag verification failed" ) ]
1017 ChecksumFailure ,
1118
19+ /// The in-memory epoch keyring has no master key installed for the
20+ /// requested `(mk_epoch, cipher_id)` pair. Happens when a handle is
21+ /// asked to decrypt or derive under an epoch it was never given key
22+ /// material for — typically a rekey counterpart key that was not
23+ /// supplied on resume. The caller must install the missing epoch's key
24+ /// (e.g. via the rekey resume path, which takes both KEKs) before
25+ /// retrying.
1226 #[ error( "required persisted key is unavailable: mk_epoch={mk_epoch} cipher_id={cipher_id}" ) ]
1327 MissingPersistedKey { mk_epoch : u64 , cipher_id : u8 } ,
1428
1529 #[ error( "corruption: {0:?}" ) ]
1630 Corruption ( CorruptionDetail ) ,
1731
32+ /// A realm exceeded one of the caps recorded in its
33+ /// [`RealmQuotas`](crate::RealmQuotas) row — page count, dirty-page
34+ /// count, scratch-page count, or segment bytes, per `kind`. The
35+ /// transaction that would have crossed the limit is refused; the caller
36+ /// must free space in that realm (delete data, commit and let
37+ /// reclamation run) or raise the configured quota before retrying.
1838 #[ error( "quota exceeded: realm={realm:?} kind={kind:?} used={used} limit={limit}" ) ]
1939 Quota {
2040 realm : RealmId ,
@@ -23,36 +43,91 @@ pub enum PagedbError {
2343 limit : u64 ,
2444 } ,
2545
46+ /// The VFS backend reported the underlying storage device or filesystem
47+ /// is full. Not raised by any production code path today — it exists
48+ /// for VFS backends to report device-level exhaustion distinctly from a
49+ /// generic [`Self::Io`] error; a caller that sees it should free disk
50+ /// space or point the store at a volume with headroom.
2651 #[ error( "no space (VFS-level exhaustion)" ) ]
2752 NoSpace ,
2853
54+ /// A nonce generator's 48-bit per-file counter reached its maximum and
55+ /// cannot issue another nonce without risking reuse under the same key.
56+ /// The file (main.db or a segment) must be rekeyed to a fresh epoch
57+ /// before any further page can be encrypted into it.
2958 #[ error( "nonce counter exhausted (per-file 2^48 limit reached); rekey required" ) ]
3059 NonceCounterExhausted ,
3160
61+ /// An internal size or offset computation — page offset, extent
62+ /// capacity, length conversion between integer widths — would have
63+ /// overflowed. `operation` names what was being computed. Not
64+ /// recoverable by retrying with the same input; the caller must reduce
65+ /// whatever value (payload size, page count) drove the computation out
66+ /// of range.
3267 #[ error( "arithmetic overflow while computing {operation}" ) ]
3368 ArithmeticOverflow { operation : & ' static str } ,
3469
70+ /// The handle is open in a mode that does not permit the attempted
71+ /// write. Raised by VFS backends when a file was opened read-only and by
72+ /// the pager for a handle without write access. Only `Standalone` and
73+ /// `Follower` handles may write; `ReadOnly` and `Observer` handles must
74+ /// reopen (or promote, for a frozen reader) before writing.
3575 #[ error( "read-only handle" ) ]
3676 ReadOnly ,
3777
78+ /// A frozen-reader (`ReadOnly`) handle tried to acquire the writer
79+ /// sentinel while a `Standalone` or `Follower` writer already holds it.
80+ /// Only one writer may be open on a store at a time; retry once the
81+ /// existing writer closes, or open in a mode that does not need the
82+ /// writer lock.
3883 #[ error( "writer already present" ) ]
3984 WriterPresent ,
4085
86+ /// A `Standalone`/`Follower` writer, or a `ReadOnly` promotion to
87+ /// `Follower`, tried to acquire the writer sentinel while a frozen
88+ /// (`ReadOnly`) reader already holds the frozen-readers lock. Writer
89+ /// modes and frozen-reader mode are mutually exclusive on the same
90+ /// store; retry once every frozen reader closes.
4191 #[ error( "readers present" ) ]
4292 ReadersPresent ,
4393
94+ /// A writer-mode handle (`Standalone` or `Follower`) tried to acquire
95+ /// the writer sentinel while another writer already holds it. Distinct
96+ /// from [`Self::WriterPresent`], which is the frozen-reader side of the
97+ /// same contention. Only one writer handle may be open on a store at a
98+ /// time; close the existing writer first.
4499 #[ error( "already open" ) ]
45100 AlreadyOpen ,
46101
102+ /// A shared or exclusive VFS-level lock could not be acquired because
103+ /// another handle already holds a conflicting lock on the same path.
104+ /// Transient — retry after the contending handle releases the lock, or
105+ /// treat as a longer-lived open-mode conflict if it persists.
47106 #[ error( "path lock contention" ) ]
48107 AlreadyLocked ,
49108
109+ /// `Db::open` found a directory left by an interrupted `restore_from`
110+ /// that was never promoted to a live store. Restored directories must be
111+ /// explicitly promoted before they can be opened normally; open with the
112+ /// restore-completion path (or discard the directory and restore again)
113+ /// instead of the standard open.
50114 #[ error( "restored directory not promoted" ) ]
51115 RestoredNotPromoted ,
52116
117+ /// `apply_incremental` was called on a handle that is not in `Follower`
118+ /// mode. Only a `Follower` handle has the base-commit identity an
119+ /// incremental apply reconciles against; open the handle as `Follower`
120+ /// (e.g. via `promote_to_follower`) before calling `apply_incremental`.
53121 #[ error( "identity forked; apply_incremental refused" ) ]
54122 IdentityForked ,
55123
124+ /// An incremental snapshot's manifest disagrees with this handle's
125+ /// current identity or reader-visible state in a way that makes the
126+ /// snapshot inapplicable — `field` names which check failed (e.g. a
127+ /// mismatched root or base commit). Distinct from
128+ /// [`CorruptionDetail::SnapshotArtifactInvalid`]: the manifest itself is
129+ /// well-formed, it just does not describe a target this handle can
130+ /// reach. Apply a snapshot whose base matches this handle's state.
56131 #[ error( "incremental snapshot is incompatible: {field}" ) ]
57132 SnapshotIncompatible { field : & ' static str } ,
58133
@@ -76,46 +151,104 @@ pub enum PagedbError {
76151 #[ error( "incremental snapshot would overwrite base-live page {page_id}" ) ]
77152 SnapshotBasePageReused { page_id : u64 } ,
78153
154+ /// The durable A/B header names a commit whose apply-journal actions
155+ /// (a pending incremental-apply reconciliation) could not be replayed or
156+ /// reconciled on this open. The handle is poisoned at `commit`; the
157+ /// remedy is a fresh `Db::open`, which retries journal replay from
158+ /// scratch rather than continuing on a handle that may hold
159+ /// partially-applied in-memory state.
79160 #[ error( "commit {commit:?} is durable but unpublished; reopen required" ) ]
80161 DurablyCommittedButUnpublished { commit : CommitId } ,
81162
163+ /// A rekey resume began writing pages under the target epoch — an
164+ /// operation with no safe in-process rollback — and then failed before
165+ /// recovery could complete. The handle is poisoned at `commit` and
166+ /// `source` carries the underlying failure. The remedy is the same as
167+ /// [`Self::DurablyCommittedButUnpublished`]: reopen the store, which
168+ /// drives recovery from the durable header rather than resuming on a
169+ /// handle with mixed-epoch state in flight.
82170 #[ error( "rekey activated a target epoch at commit {commit:?}; reopen required: {source}" ) ]
83171 RekeyTargetEpochActivated {
84172 commit : CommitId ,
85173 #[ source]
86174 source : Box < PagedbError > ,
87175 } ,
88176
177+ /// `begin_read_at(commit)` named a commit that has been pruned from the
178+ /// commit-history index (by [`RetainPolicy`](crate::RetainPolicy)) and
179+ /// is no longer reachable. `oldest_available` names the oldest commit a
180+ /// point-in-time read can still target; request a commit at or after it,
181+ /// or widen the retention policy before the commit is needed again.
89182 #[ error( "commit {commit:?} gone; oldest_available={oldest_available:?}" ) ]
90183 CommitGone {
91184 commit : CommitId ,
92185 oldest_available : CommitId ,
93186 } ,
94187
188+ /// No row or resource matched the given key. Reused across several
189+ /// lookups: a catalog/segment row absent from its tree, a `main.db`
190+ /// missing under a mode that requires it to already exist, or a segment
191+ /// extent index with no entry at the requested `start_page_id`.
192+ /// Check the operation that raised it for which of these applies; the
193+ /// caller's next step is usually to treat the key as absent rather than
194+ /// retry.
95195 #[ error( "not found" ) ]
96196 NotFound ,
97197
198+ /// `link_segment` was called with a `name` that already has a catalog
199+ /// row in this realm. Names are unique per realm; `unlink_segment` or
200+ /// `replace_segment` the existing entry first, or choose a different
201+ /// name.
98202 #[ error( "already linked" ) ]
99203 AlreadyLinked ,
100204
205+ /// `unlink_segment` or `replace_segment` named a segment that has no
206+ /// catalog row in this realm — either it was never linked or a
207+ /// concurrent operation already removed it. Nothing to do; the caller
208+ /// should not retry the same unlink.
101209 #[ error( "not linked" ) ]
102210 NotLinked ,
103211
212+ /// A segment or counter name exceeds `MAX_SEGMENT_NAME_LEN` bytes.
213+ /// Shorten the name before retrying; the catalog key encoding has no
214+ /// escape for longer names.
104215 #[ error( "name too long" ) ]
105216 NameTooLong ,
106217
218+ /// A page read or cache access was asked for a page kind that does not
219+ /// belong to the file it targets (a segment kind requested through the
220+ /// main-db read path, or vice versa), or a decrypted page's kind byte
221+ /// did not match any kind the caller was willing to accept. Indicates a
222+ /// caller bug rather than on-disk corruption — the authenticated
223+ /// envelope kind mismatches are instead reported as
224+ /// [`CorruptionDetail::NodeKindMismatch`].
107225 #[ error( "illegal page kind for segment" ) ]
108226 IllegalPageKind ,
109227
228+ /// A value, key, or extent count would not fit its on-disk encoding —
229+ /// a leaf record exceeding the page's capacity even as an overflow
230+ /// reference, a separator too long for an internal node, or an extent
231+ /// page count that overflows `u32`. Shrink the value/key or split the
232+ /// write into multiple extents before retrying.
110233 #[ error( "payload too large" ) ]
111234 PayloadTooLarge ,
112235
236+ /// `SegmentWriter::append_extent` was called with an empty page list.
237+ /// An extent must span at least one page; pass a non-empty slice.
113238 #[ error( "extent must contain at least one page" ) ]
114239 EmptyExtent ,
115240
241+ /// A segment footer manifest exceeds the format's maximum manifest
242+ /// length for the segment's page size. Shrink the manifest payload
243+ /// before calling `set_manifest` / sealing the segment.
116244 #[ error( "manifest too large" ) ]
117245 ManifestTooLarge ,
118246
247+ /// `mmap_view` was asked to map `segment_bytes` of decrypted scratch but
248+ /// only `available_bytes` remain under
249+ /// [`OpenOptions::mmap_view_scratch_bytes`](crate::OpenOptions). Drop an
250+ /// existing `MmapView` to free budget, request a smaller extent, or
251+ /// raise the configured budget.
119252 #[ error(
120253 "mmap-view quota exceeded: segment_bytes={segment_bytes} available_bytes={available_bytes}"
121254 ) ]
@@ -124,6 +257,14 @@ pub enum PagedbError {
124257 available_bytes : u64 ,
125258 } ,
126259
260+ /// Under [`ReaderStallPolicy::AbortOldest`](crate::ReaderStallPolicy),
261+ /// this is the oldest conflicting reader whose pin is blocking
262+ /// reclamation the writer needs — its next operation is aborted so the
263+ /// writer can proceed. Also returned by a `MainDbNonceGen` when a nonce
264+ /// is requested past the current anchor budget and the caller must
265+ /// persist `pending_anchor()` and call `commit_anchor` before issuing
266+ /// more. In the reader case, drop the reader and retry with a fresh
267+ /// snapshot; in the nonce case, commit the pending anchor first.
127268 #[ error( "aborted (reader stall policy)" ) ]
128269 Aborted ,
129270
@@ -157,15 +298,37 @@ pub enum PagedbError {
157298 oldest_pinning_commit : u64 ,
158299 } ,
159300
301+ /// Under [`ReaderStallPolicy::Reject`](crate::ReaderStallPolicy), a new
302+ /// write would need free-list pages that reader pins are preventing the
303+ /// writer from reclaiming, and no free pages remain. No production call
304+ /// site raises this today (`Reject` policy's free-list path is exercised
305+ /// only in `tests/smoke.rs`'s Display-coverage test); when it is wired
306+ /// up, the caller's remedy is to wait for the pinning readers to close
307+ /// or switch to `AbortOldest`.
160308 #[ error( "free list exhausted" ) ]
161309 FreeListExhausted ,
162310
311+ /// Under [`ReaderStallPolicy::Reject`](crate::ReaderStallPolicy), a
312+ /// segment tombstone cannot be finalized because a reader still pins the
313+ /// truncated range. No production call site raises this today (see
314+ /// [`Self::FreeListExhausted`] — same policy, same test-only coverage);
315+ /// the intended remedy is to wait for the pinning reader to close.
163316 #[ error( "segment tombstone stalled by reader pin" ) ]
164317 SegmentTombstoneStalled ,
165318
319+ /// An apply-journal reconciliation deferred a segment tombstone because
320+ /// a reader still pins the range being truncated; the durable target is
321+ /// published for new readers, but this transaction returns the error to
322+ /// signal the tombstone did not complete synchronously. Retry
323+ /// `retry_pending_apply_journal` (implicitly driven by later opens/GC)
324+ /// once the pinning reader closes.
166325 #[ error( "readers pinning truncated range" ) ]
167326 ReadersPinningTruncatedRange ,
168327
328+ /// Resuming a rekey whose source and target epochs use different KEKs
329+ /// (`same_kek == false`) requires both the primary and counterpart KEK,
330+ /// but only the primary was supplied. Retry the resume with the
331+ /// counterpart KEK for `source_epoch`/`target_epoch` also provided.
169332 #[ error(
170333 "rekey resume requires counterpart key for source epoch {source_epoch} and target epoch {target_epoch}"
171334 ) ]
@@ -174,6 +337,11 @@ pub enum PagedbError {
174337 target_epoch : u64 ,
175338 } ,
176339
340+ /// The KEK(s) supplied to resume a rekey do not reproduce the durable
341+ /// intent's HK proof for `source_epoch` and/or `target_epoch` — either
342+ /// the wrong KEK was supplied, or (for a same-KEK rekey) the single
343+ /// supplied KEK cannot derive both epochs' header keys. Supply the KEK
344+ /// that was active when the rekey was started.
177345 #[ error(
178346 "rekey counterpart key does not prove source epoch {source_epoch} for target epoch {target_epoch}"
179347 ) ]
@@ -182,9 +350,25 @@ pub enum PagedbError {
182350 target_epoch : u64 ,
183351 } ,
184352
353+ /// The durable rekey-intent or rekey-progress catalog row cannot be
354+ /// admitted — `field` names the check that failed (a missing intent row,
355+ /// an unexpected `target_mk_epoch`, source/target cipher mismatch, or a
356+ /// stage that still has segments pending when none were expected). This
357+ /// is a recorded-state precondition failure, not necessarily on-disk
358+ /// corruption of the row's bytes (that is
359+ /// [`CorruptionDetail::CatalogRowInvalid`]); it means the rekey cannot
360+ /// safely proceed from where it claims to be. Inspect the rekey state
361+ /// and resume from a consistent stage, or restart the rekey.
185362 #[ error( "recorded rekey state is invalid: {field}" ) ]
186363 RekeyStateInvalid { field : & ' static str } ,
187364
365+ /// A rekey's replacement segment file (named by durable progress) could
366+ /// not be opened, is absent from both the live and staging locations, or
367+ /// fails its size/geometry sanity checks after opening. Because the
368+ /// replacement id is durable progress, this fails closed rather than
369+ /// regenerating the file — the caller must restore the missing
370+ /// replacement file (e.g. from backup) or restart the rekey for this
371+ /// segment.
188372 #[ error( "recorded rekey replacement segment {replacement_segment_id:?} is missing or invalid" ) ]
189373 RekeyReplacementMissing { replacement_segment_id : [ u8 ; 16 ] } ,
190374
@@ -201,12 +385,26 @@ pub enum PagedbError {
201385 detail : & ' static str ,
202386 } ,
203387
388+ /// The requested operation is not implemented by the current backend or
389+ /// target — e.g. `mmap_view` on WASM (no native mmap), an unknown
390+ /// on-wire cipher id, or `promote_to_follower` called on a handle that
391+ /// is not `ReadOnly`. Check the operation's docs for which
392+ /// backends/modes support it; there is no runtime workaround short of
393+ /// switching backend, target, or handle mode.
204394 #[ error( "unsupported by backend" ) ]
205395 Unsupported ,
206396
397+ /// The platform's cryptographically secure RNG failed to produce
398+ /// randomness (nonces, salts, key material). Propagated verbatim from
399+ /// `getrandom`. Not caller-recoverable beyond retrying — it indicates
400+ /// the OS entropy source itself is unavailable or refused the request.
207401 #[ error( "cryptographically secure randomness unavailable: {0}" ) ]
208402 Randomness ( #[ from] getrandom:: Error ) ,
209403
404+ /// An underlying `std::io::Error` from the VFS layer (open, read, write,
405+ /// sync, rename, lock) that does not map to any of the more specific
406+ /// variants above. Inspect the wrapped error's `kind()` for the
407+ /// platform-reported cause.
210408 #[ error( "io: {0}" ) ]
211409 Io ( #[ from] std:: io:: Error ) ,
212410}
0 commit comments