Skip to content

Commit 932aa81

Browse files
committed
docs: expand error-variant and overflow-page guidance comments
Document every PagedbError variant with its raising call sites and the caller's recovery path, and clarify stale wording in overflow-chain test comments. No functional change.
1 parent 127d203 commit 932aa81

3 files changed

Lines changed: 210 additions & 5 deletions

File tree

src/btree/tests/overflow_walk_offset.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! Regression: the overflow-chain walk (`collect_all_page_ids` /
22
//! `collect_overflow_chain`, and the strict `find_dangling` invariant helper)
3-
//! must read a v2 overflow ROOT page's `next` pointer at the correct offset.
3+
//! must read an overflow ROOT page's `next` pointer at the correct offset.
44
//!
5-
//! A v2 root is laid out `refcount[4] || next[8] || …`; a chain page is
5+
//! A root is laid out `refcount[4] || next[8] || …`; a chain page is
66
//! `next[8] || …`. Reading byte 0 uniformly decoded the root's `refcount` (1 for
77
//! a single-owner value) as the next page id, spuriously "chaining" to reserved
88
//! page 1 and failing AEAD — a false positive that looked like store corruption.

src/errors.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,35 @@ use crate::{CommitId, RealmId};
66
#[non_exhaustive]
77
#[derive(Debug, thiserror::Error)]
88
pub 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
}

tests/overflow_refcount.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
//! Integration tests for v2 overflow root pages with reference counting.
2-
//! Verifies that shared overflow chains survive partial deletions and are
3-
//! freed exactly when the last reference is dropped.
1+
//! Integration tests for v2 overflow chains (values that spill past a single
2+
//! page). Covers: writing and reading a value that spans two or three
3+
//! overflow pages, an overwrite freeing the old chain, a delete freeing the
4+
//! chain, inline and overflow values coexisting in the same tree, and a mix
5+
//! of overflow writes and deletes across many keys leaving unrelated keys
6+
//! uncorrupted. Every chain here is written with `refcount = 1` — no test
7+
//! shares a chain across multiple live references, and overflow-chain
8+
//! lifetime is not governed by refcounting: a CoW leaf that duplicates an
9+
//! overflow pointer is protected by the same commit-tagged deferred
10+
//! reclamation that protects the old leaf page.
411
512
use pagedb::vfs::memory::MemVfs;
613
use pagedb::{Db, RealmId};

0 commit comments

Comments
 (0)