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
// SPDX-License-Identifier: BUSL-1.1
//! Why a published checkpoint generation could not be decoded.
//!
//! One type across every engine's load path because they all answer the same
//! question the same way: a generation decodes WHOLE or not at all, and the
//! reason must name the specific corruption it found. A boot-time restore that
//! reports "the generation is bad" without saying which file, which value, and
//! what was expected leaves an operator staring at a full WAL replay with no way
//! to tell a truncated write from a format skew from a hand-edited data dir.
use std::path::PathBuf;
use crate::bridge::envelope::ErrorCode;
/// A corruption found while decoding one engine's checkpoint generation.
///
/// Every variant names a closed, specific failure — there is deliberately no
/// free-form catch-all, because the caller's only response is to abandon the
/// restore and replay the whole WAL, and the log line it emits is then the sole
/// record of what was actually wrong with the data dir.
#[derive(Debug, thiserror::Error)]
pub(crate) enum CheckpointDecodeError {
/// The generation directory itself could not be listed.
#[error("cannot scan {}: {source}", dir.display())]
ScanDir {
dir: PathBuf,
#[source]
source: std::io::Error,
},
/// One entry within an otherwise readable generation directory faulted.
#[error("cannot read dir entry: {source}")]
DirEntry {
#[source]
source: std::io::Error,
},
/// The filename does not encode the key its contents restore under. Guessing
/// a key would install the state where nothing ever reads it.
#[error("unparseable checkpoint filename {stem:?}")]
UnparseableFilename { stem: String },
/// A checkpoint file could not be read off disk.
#[error("cannot read {}: {source}", path.display())]
ReadFile {
path: PathBuf,
#[source]
source: nodedb_wal::WalError,
},
/// A checkpoint file's MessagePack body did not decode.
#[error("cannot decode {}: {source}", path.display())]
MsgpackDecode {
path: PathBuf,
#[source]
source: zerompk::Error,
},
/// The file was written by a build whose layout this one does not share.
/// Reading it anyway would misinterpret every field after the skew.
#[error("{} has format version {found}, expected {expected}", path.display())]
FormatVersion {
path: PathBuf,
found: u16,
expected: u16,
},
/// A sparse-vector index file did not decode into an index. The decoder
/// reports only success or failure, so there is no inner detail to carry.
#[error("cannot decode {}", path.display())]
UndecodableIndex { path: PathBuf },
/// A columnar snapshot decoded but does not describe a rebuildable engine.
///
/// Boxed: `ColumnarError` is the widest payload any variant here carries, and
/// these errors travel in a `Result` on the boot path.
#[error("cannot rebuild engine from {}: {source}", path.display())]
EngineNotRebuildable {
path: PathBuf,
#[source]
source: Box<nodedb_columnar::ColumnarError>,
},
/// The flushed segment blobs and their surrogate sidecar disagree in length.
/// The two are held in lockstep by index, so a partial sidecar has no
/// positional meaning at all.
#[error(
"{} has {segments} flushed segments but {surrogates} surrogate entries; the two are \
held in lockstep by index and a partial sidecar cannot be positionally interpreted",
path.display()
)]
SurrogateLockstepMismatch {
path: PathBuf,
segments: usize,
surrogates: usize,
},
/// A KV collection file decoded, but its index registrations did not rebuild.
/// Carries the inner corruption so the file AND the bad value are both named.
#[error("cannot rebuild indexes from {}: {source}", path.display())]
KvIndexes {
path: PathBuf,
#[source]
source: Box<CheckpointDecodeError>,
},
/// A composite index's positions and fields do not line up, so no position
/// can be attributed to a field.
#[error("composite index {fields:?} has {positions} field positions for {field_count} fields")]
CompositeFieldPositionMismatch {
fields: Vec<String>,
positions: usize,
field_count: usize,
},
/// A sort direction outside the closed set the exporter writes. Refused
/// rather than defaulted: the builder reads every non-"DESC" spelling as
/// ascending, so accepting one would silently invert the index.
#[error("sorted index {index:?} column {column:?} has unknown direction {direction:?}")]
UnknownSortDirection {
index: String,
column: String,
direction: String,
},
/// A window type outside the closed set the exporter writes. Refused for the
/// same reason: an unknown one reads as unwindowed, silently widening the
/// index to every entry ever written to it.
#[error("sorted index {index:?} has unknown window type {window_type:?}")]
UnknownWindowType { index: String, window_type: String },
/// The exported sort definition was rejected by the same builder the live
/// registration path uses, so a restored def could not match a registered one.
///
/// The code is boxed for the same reason the bridge's `Response` boxes it.
#[error("sorted index {index:?} is not rebuildable: {code:?}")]
SortedIndexNotRebuildable { index: String, code: Box<ErrorCode> },
/// An on-disk field position does not fit this machine's `usize`. The file
/// holds `u64` so it does not encode the writer's pointer width; a value that
/// does not fit is a corrupt file, not something to clamp.
#[error("index on {field:?} has out-of-range field position {position}")]
FieldPositionOutOfRange { field: String, position: u64 },
}
impl From<CheckpointDecodeError> for crate::Error {
fn from(e: CheckpointDecodeError) -> Self {
crate::Error::SegmentCorrupted {
detail: e.to_string(),
}
}
}