1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
//! Top-level error type.
use crate::layout::BlobGuid;
use crate::store::{AllocError, FreeError};
/// Result alias used throughout the crate.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Top-level error type covering the union of every failure mode.
///
/// Marked `#[non_exhaustive]` — new variants may be added in
/// minor releases without breaking SemVer. Callers should always
/// match with a `_` arm.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// BlobStore I/O failure.
BlobStoreIo(std::io::Error),
/// Bump allocator / slot table exhaustion / invalid alloc.
Alloc(AllocError),
/// Free-list misuse.
Free(FreeError),
/// Key longer than `u16::MAX` bytes.
KeyTooLong {
/// Caller-supplied length.
len: usize,
},
/// Value longer than `u16::MAX` bytes.
ValueTooLong {
/// Caller-supplied length.
len: usize,
},
/// A walker arm hit a code path that the engine doesn't yet
/// implement (e.g. degenerate `Leaf` / `EmptyRoot` spillover
/// or strict-prefix ART insert cases). The static string names
/// the unimplemented case for diagnostics.
NotYetImplemented(&'static str),
/// An internal invariant the engine relies on was observed
/// to be violated — typically a background thread closing a
/// channel it shouldn't have closed, or a completion sender
/// disappearing without producing a result. Distinct from
/// [`Self::NotYetImplemented`] (genuine feature gap) and
/// from [`Self::NodeCorrupt`] (on-disk / cache layout
/// problem). The static string names the specific invariant
/// for triage.
Internal(&'static str),
/// A blob's slot table or header is corrupt — recovery
/// should bail out rather than silently misbehave.
///
/// Construct via [`Error::node_corrupt`] and optionally
/// enrich with [`Error::with_blob_guid`] / [`Error::with_slot`]
/// when the surrounding code path knows the affected blob
/// or slot. The buffer manager and walker entry points
/// automatically attach blob context where they have it.
NodeCorrupt {
/// Static description of where the corruption was detected.
context: &'static str,
/// GUID of the blob that exposed the corruption, when
/// the propagating code path knows it. `None` for low-
/// level helpers that don't have blob context (e.g. raw
/// `BlobFrame::wrap` on a stack buffer).
blob_guid: Option<BlobGuid>,
/// Slot inside `blob_guid` that exposed the corruption,
/// when applicable. `None` for header / body-level
/// problems.
slot: Option<u16>,
},
/// WAL replay encountered a record whose sanity validation
/// failed — record magic mismatch, CRC32 mismatch, unknown
/// variant tag, truncated body, etc.
ReplaySanityFailed {
/// What went wrong (decoder-supplied static string).
context: &'static str,
/// Position in the journal file where the bad record
/// starts. `0` when the codec is invoked on a raw
/// in-memory buffer and the caller hasn't supplied an
/// offset.
record_offset: u64,
},
/// `Tree::rename` (or similar) called with a `src` that has no
/// leaf in the tree.
NotFound,
/// `Tree::rename(.., force=false)` called with a `dst` that
/// already has a leaf. Caller can retry with `force=true` to
/// overwrite.
DstExists,
/// A scoped [`crate::View`] read tried to access a key or range
/// prefix outside the subtree captured when the view was opened.
OutsideViewScope {
/// Length of the requested key or prefix.
requested_len: usize,
/// Length of the view's captured prefix.
scope_len: usize,
},
}
impl Error {
/// Construct a [`Error::NodeCorrupt`] with the given static
/// context but no blob / slot metadata. Layers higher in the
/// stack (buffer manager, walker entry points) typically
/// enrich the error via [`Self::with_blob_guid`] /
/// [`Self::with_slot`] before it surfaces to the caller.
#[must_use]
pub const fn node_corrupt(context: &'static str) -> Self {
Self::NodeCorrupt {
context,
blob_guid: None,
slot: None,
}
}
/// Attach a `blob_guid` to a [`Self::NodeCorrupt`] error
/// without overwriting one set by a deeper layer. No-op for
/// other variants — safe to chain unconditionally on any
/// `Error` value.
#[must_use]
pub fn with_blob_guid(mut self, guid: BlobGuid) -> Self {
if let Self::NodeCorrupt { blob_guid, .. } = &mut self {
if blob_guid.is_none() {
*blob_guid = Some(guid);
}
}
self
}
/// Attach a `slot` index to a [`Self::NodeCorrupt`] error
/// without overwriting one set by a deeper layer.
#[must_use]
pub fn with_slot(mut self, slot_index: u16) -> Self {
if let Self::NodeCorrupt { slot, .. } = &mut self {
if slot.is_none() {
*slot = Some(slot_index);
}
}
self
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BlobStoreIo(e) => write!(f, "store I/O: {e}"),
Self::Alloc(e) => write!(f, "alloc: {e}"),
Self::Free(e) => write!(f, "free: {e}"),
Self::KeyTooLong { len } => write!(f, "key too long ({len} bytes; max {})", u16::MAX),
Self::ValueTooLong { len } => {
write!(f, "value too long ({len} bytes; max {})", u16::MAX)
}
Self::NotYetImplemented(where_) => write!(f, "not yet implemented: {where_}"),
Self::Internal(what) => write!(f, "internal invariant violated: {what}"),
Self::NodeCorrupt {
context,
blob_guid,
slot,
} => {
write!(f, "node corrupt at {context}")?;
if let Some(g) = blob_guid {
// First 4 bytes is enough to disambiguate in
// logs without dumping the full 16-byte tag.
write!(f, " (blob={:02x?})", &g[..4])?;
}
if let Some(s) = slot {
write!(f, " (slot={s})")?;
}
Ok(())
}
Self::ReplaySanityFailed {
context,
record_offset,
} => {
write!(
f,
"WAL replay sanity-check failed at offset {record_offset}: {context}"
)
}
Self::NotFound => write!(f, "key not found"),
Self::DstExists => write!(
f,
"destination key already exists (use force=true to overwrite)"
),
Self::OutsideViewScope {
requested_len,
scope_len,
} => write!(
f,
"view access outside captured scope (requested {requested_len} bytes, scope {scope_len} bytes)"
),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::BlobStoreIo(e) => Some(e),
Self::Alloc(e) => Some(e),
Self::Free(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Self::BlobStoreIo(e)
}
}
impl From<AllocError> for Error {
fn from(e: AllocError) -> Self {
Self::Alloc(e)
}
}
impl From<FreeError> for Error {
fn from(e: FreeError) -> Self {
Self::Free(e)
}
}
#[cfg(test)]
mod tests {
use std::error::Error as StdError;
use std::io;
use super::*;
#[test]
fn node_corrupt_context_is_enriched_once() {
let err = Error::node_corrupt("slot decode")
.with_blob_guid([0xAB; 16])
.with_blob_guid([0xCD; 16])
.with_slot(7)
.with_slot(9);
let rendered = err.to_string();
assert!(rendered.contains("node corrupt at slot decode"));
assert!(rendered.contains("blob=[ab, ab, ab, ab]"));
assert!(rendered.contains("slot=7"));
assert!(!rendered.contains("cd"));
assert!(!rendered.contains("slot=9"));
}
#[test]
fn display_covers_public_error_variants() {
let cases = [
(
Error::KeyTooLong { len: 70_000 }.to_string(),
"key too long (70000 bytes; max 65535)",
),
(
Error::ValueTooLong { len: 70_001 }.to_string(),
"value too long (70001 bytes; max 65535)",
),
(
Error::NotYetImplemented("strict prefix").to_string(),
"not yet implemented: strict prefix",
),
(
Error::Internal("lost dirty image").to_string(),
"internal invariant violated: lost dirty image",
),
(
Error::ReplaySanityFailed {
context: "bad CRC",
record_offset: 42,
}
.to_string(),
"WAL replay sanity-check failed at offset 42: bad CRC",
),
(Error::NotFound.to_string(), "key not found"),
(
Error::DstExists.to_string(),
"destination key already exists (use force=true to overwrite)",
),
];
for (actual, expected) in cases {
assert_eq!(actual, expected);
}
}
#[test]
fn error_sources_are_exposed_for_wrapped_errors_only() {
let blob = Error::from(io::Error::other("disk"));
assert!(blob.source().is_some());
let alloc = Error::from(AllocError::OutOfSlots);
assert!(alloc.source().is_some());
assert_eq!(
alloc.to_string(),
format!("alloc: {}", AllocError::OutOfSlots)
);
let free = Error::from(FreeError::InvalidSlot(99));
assert!(free.source().is_some());
assert_eq!(free.to_string(), "free: free_node: invalid slot index 99");
assert!(Error::NotFound.source().is_none());
assert!(Error::DstExists.source().is_none());
}
}