zeppelin_embed/wal/replay.rs
1//! Checked WAL prefix replay.
2
3use super::LogSeq;
4use super::header::{WAL_HEADER_LEN, WalHeaderError, decode_header};
5use super::record::{RecordError, WalRecord, decode_record};
6
7/// Whether a corrupt record is the final observable record or precedes a
8/// checksum-valid successor.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum CorruptionLocation {
11 /// No checksum-valid successor was found at the declared next boundary.
12 Tail,
13 /// A checksum-valid record follows the corrupt record.
14 Middle,
15}
16
17/// Specific corruption signal that ended replay.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum CorruptionReason {
20 /// Record framing or checksum validation failed.
21 Record {
22 /// Tail versus proven middle corruption.
23 location: CorruptionLocation,
24 /// Exact record validation failure.
25 error: RecordError,
26 },
27 /// The first valid record disagreed with the sequence declared by the file header.
28 FirstSequenceMismatch {
29 /// Sequence declared by the WAL file header.
30 expected: LogSeq,
31 /// Sequence observed in the first checked record.
32 actual: LogSeq,
33 /// Tail versus proven middle corruption.
34 location: CorruptionLocation,
35 },
36 /// A valid record skipped one or more required sequence numbers.
37 SequenceGap {
38 /// Required next sequence.
39 expected: LogSeq,
40 /// Sequence observed in the checked record.
41 actual: LogSeq,
42 /// Tail versus proven middle corruption.
43 location: CorruptionLocation,
44 },
45 /// A valid record repeated or regressed below the required sequence.
46 SequenceRegression {
47 /// Required next sequence.
48 expected: LogSeq,
49 /// Sequence observed in the checked record.
50 actual: LogSeq,
51 /// Tail versus proven middle corruption.
52 location: CorruptionLocation,
53 },
54}
55
56/// Why replay stopped after its trusted prefix.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum ReplayTerminator {
59 /// WAL file-header validation failed before record replay began.
60 InvalidHeader(WalHeaderError),
61 /// All bytes ended exactly after a valid record, or the log was empty.
62 CleanEnd,
63 /// Replay stopped before consuming the named invalid record.
64 CorruptAt {
65 /// Byte offset of the first invalid record.
66 offset: usize,
67 /// Specific validation or sequence failure.
68 reason: CorruptionReason,
69 },
70}
71
72/// A checksum- and sequence-validated prefix plus its explicit terminator.
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct ReplayResult<'a> {
75 /// Records preceding the first invalid record.
76 pub records: Vec<WalRecord<'a>>,
77 /// Clean end versus typed corruption boundary.
78 pub terminator: ReplayTerminator,
79}
80
81/// Replays the largest trusted prefix of an append-only WAL.
82///
83/// Every record considered for return is checksum-checked. Replay never skips
84/// an invalid record. If a checksum-valid record begins at the invalid
85/// record's declared successor boundary, the reason is classified as
86/// [`CorruptionLocation::Middle`], but that successor is not returned. A torn
87/// tail or corruption without such a successor is classified as `Tail`.
88/// The file header declares the first required sequence, and every subsequent
89/// record must increment by exactly one; gaps, duplicates, and regressions
90/// terminate the trusted prefix.
91#[must_use]
92pub fn replay(bytes: &[u8]) -> ReplayResult<'_> {
93 let mut records = Vec::new();
94 let header = match decode_header(bytes) {
95 Ok(header) => header,
96 Err(error) => {
97 return ReplayResult {
98 records,
99 terminator: ReplayTerminator::InvalidHeader(error),
100 };
101 }
102 };
103 let mut offset = WAL_HEADER_LEN;
104 let mut expected = header.first_seq;
105 while let Some(remaining) = bytes
106 .get(offset..)
107 .filter(|remaining| !remaining.is_empty())
108 {
109 let decoded = match decode_record(remaining) {
110 Ok(decoded) => decoded,
111 Err(error) => {
112 let location = error
113 .framed_length()
114 .and_then(|length| offset.checked_add(length))
115 .map_or(CorruptionLocation::Tail, |next| {
116 successor_location(bytes, next)
117 });
118 return ReplayResult {
119 records,
120 terminator: ReplayTerminator::CorruptAt {
121 offset,
122 reason: CorruptionReason::Record { location, error },
123 },
124 };
125 }
126 };
127 let next = offset.saturating_add(decoded.encoded_len);
128 if decoded.record.seq != expected {
129 let location = successor_location(bytes, next);
130 let reason = if records.is_empty() {
131 CorruptionReason::FirstSequenceMismatch {
132 expected,
133 actual: decoded.record.seq,
134 location,
135 }
136 } else if decoded.record.seq > expected {
137 CorruptionReason::SequenceGap {
138 expected,
139 actual: decoded.record.seq,
140 location,
141 }
142 } else {
143 CorruptionReason::SequenceRegression {
144 expected,
145 actual: decoded.record.seq,
146 location,
147 }
148 };
149 return ReplayResult {
150 records,
151 terminator: ReplayTerminator::CorruptAt { offset, reason },
152 };
153 }
154 records.push(decoded.record);
155 offset = next;
156 expected = LogSeq::new(expected.get().saturating_add(1));
157 }
158 ReplayResult {
159 records,
160 terminator: ReplayTerminator::CleanEnd,
161 }
162}
163
164fn successor_location(bytes: &[u8], offset: usize) -> CorruptionLocation {
165 bytes
166 .get(offset..)
167 .filter(|successor| !successor.is_empty())
168 .and_then(|successor| decode_record(successor).ok())
169 .map_or(CorruptionLocation::Tail, |_| CorruptionLocation::Middle)
170}