1use std::path::{Path, PathBuf};
9
10use kernel::{AxCode, AxError, GENESIS_PREV, Seq, TimeMs};
11
12use crate::chain_audit::ProofCount;
13use crate::error::{StorageError, io_err};
14use crate::real_fs::RealFs;
15use crate::verified_prefix::{ProofRecords, TailStart};
16use crate::vfs::Vfs;
17
18use super::ledger::{
19 JsonlLedger, OpenReport, SEGMENT_PREALLOCATION, SEGMENT_ROLL_BYTES, TailTruncation, WriterLock,
20 is_segment, segment_file_name, u64_count,
21};
22use super::verify::{LineCheck, LineFault};
23
24impl JsonlLedger {
25 pub fn open(dir: &Path, now: TimeMs) -> Result<(Self, OpenReport), StorageError> {
34 JsonlLedger::open_locked(dir, now, TailProof::Strict)
35 }
36
37 pub fn open_reusing(
45 dir: &Path,
46 now: TimeMs,
47 records: &ProofRecords,
48 ) -> Result<(Self, OpenReport), StorageError> {
49 JsonlLedger::open_locked(dir, now, TailProof::Records(records))
50 }
51
52 fn open_locked(
53 dir: &Path,
54 now: TimeMs,
55 proof: TailProof<'_>,
56 ) -> Result<(Self, OpenReport), StorageError> {
57 let lock = WriterLock::take(dir)?;
58 let (mut ledger, report) =
59 JsonlLedger::open_through(Box::new(RealFs::new()), dir, now, proof)?;
60 ledger.lock = Some(lock);
61 Ok((ledger, report))
62 }
63
64 #[cfg(any(test, feature = "fault"))]
77 pub fn open_faulty(
78 fs: crate::fault_fs::FaultFs,
79 dir: &Path,
80 now: TimeMs,
81 ) -> Result<(Self, OpenReport), StorageError> {
82 JsonlLedger::open_with(Box::new(fs), dir, now)
83 }
84
85 #[cfg(any(test, feature = "fault"))]
88 pub(crate) fn open_with(
89 vfs: Box<dyn Vfs>,
90 dir: &Path,
91 now: TimeMs,
92 ) -> Result<(Self, OpenReport), StorageError> {
93 JsonlLedger::open_through(vfs, dir, now, TailProof::Strict)
94 }
95
96 fn open_through(
97 vfs: Box<dyn Vfs>,
98 dir: &Path,
99 now: TimeMs,
100 proof: TailProof<'_>,
101 ) -> Result<(Self, OpenReport), StorageError> {
102 let (ledger, segments) = JsonlLedger::unopened(vfs, dir)?;
103 ledger.resume(&segments, now, proof)
104 }
105
106 fn unopened(mut vfs: Box<dyn Vfs>, dir: &Path) -> Result<(Self, Vec<PathBuf>), StorageError> {
109 vfs.create_dir_all(dir)
110 .map_err(io_err("create ledger dir", dir))?;
111 let segments: Vec<PathBuf> = vfs
112 .list(dir)
113 .map_err(io_err("list ledger dir", dir))?
114 .into_iter()
115 .filter(|p| is_segment(p))
116 .collect();
117
118 let ledger = JsonlLedger {
119 vfs,
120 dir: dir.to_path_buf(),
121 seg_path: dir.join(segment_file_name(Seq::FIRST)),
122 seg_len: 0,
123 next_seq: Seq::FIRST,
124 prev: GENESIS_PREV,
125 roll_bytes: SEGMENT_ROLL_BYTES,
126 preallocation: SEGMENT_PREALLOCATION,
127 barrier: super::barrier::Barrier::Whole,
128 sessions: crate::sessions::Sessions::for_ledger(dir),
133 observer: None,
134 lock: None,
135 halt: crate::chain_audit::ChainHalt::default(),
136 pending_unwind: None,
137 };
138 Ok((ledger, segments))
139 }
140
141 fn resume(
144 mut self,
145 segments: &[PathBuf],
146 now: TimeMs,
147 proof: TailProof<'_>,
148 ) -> Result<(Self, OpenReport), StorageError> {
149 let Some(last) = segments.last() else {
150 return Ok((
151 self,
152 OpenReport {
153 recovered: None,
154 counted: ProofCount::default(),
155 },
156 ));
157 };
158 self.probe_version(segments)?;
159 let (dropped, counted) = self.recover_tail(segments, last, proof)?;
160 let recovered = if dropped > 0 {
161 if self.next_seq != Seq::FIRST {
162 self.append_log_truncated(now, dropped)?;
163 }
164 Some(TailTruncation {
165 dropped_bytes: dropped,
166 })
167 } else {
168 None
169 };
170 Ok((self, OpenReport { recovered, counted }))
171 }
172
173 fn recover_tail(
178 &mut self,
179 segments: &[PathBuf],
180 last: &Path,
181 proof: TailProof<'_>,
182 ) -> Result<(u64, ProofCount), StorageError> {
183 let boundary = self.boundary(segments, last)?;
184 let prior = boundary.prior;
185 let start = proof
186 .start(
187 self.vfs.as_ref(),
188 last,
189 LineCheck::after(boundary.prev, boundary.next_seq),
190 )
191 .map_err(io_err("read segment", last))?;
192 let mut check = start.check;
193 let mut counted = start.counted;
194 let from = u64_count(start.from).map_err(io_err("measure segment", last))?;
195 let mut valid_len = from;
196 let mut at = start.lines;
197 let mut damaged = None;
198 let end = super::reading::scan(self.vfs.as_ref(), last, from, |line, end| {
199 at = at.saturating_add(1);
200 if let Some(first_fault) = damaged {
201 if LineCheck::carries_envelope(line) {
202 return Err(chain_refusal(last, first_fault));
203 }
204 return Ok(());
205 }
206 counted.lines_checked = counted.lines_checked.saturating_add(1);
207 let fault = match check.advance(line) {
208 Ok(_) => {
209 valid_len = end;
210 return Ok(());
211 }
212 Err(fault) => fault,
213 };
214 let source = match fault {
215 LineFault::VersionAhead(v) => {
216 return Err(StorageError::VersionAhead {
217 path: last.to_path_buf(),
218 v,
219 });
220 }
221 LineFault::NotAVersion(v) => unversioned(v),
222 LineFault::NotALine(_) => {
223 if LineCheck::carries_envelope(line) {
224 return Err(chain_refusal(last, at));
225 }
226 damaged = Some(at);
227 return Ok(());
228 }
229 LineFault::ChainBreak | LineFault::SeqGap { .. } => {
230 return Err(chain_refusal(last, at));
231 }
232 other @ (LineFault::UnknownKind(_)
233 | LineFault::NotCanonical(_)
234 | LineFault::SeqExhausted(_)) => other.into_ax(at),
235 };
236 Err(StorageError::Envelope {
237 path: last.to_path_buf(),
238 line: at,
239 source,
240 })
241 })?;
242 counted.bytes_read = counted
243 .bytes_read
244 .saturating_add(end.total.saturating_sub(from));
245 let total = end.total;
246 let run_prev = check.prev();
247 let run_seq = check.expected();
248
249 let dropped = end.filled.saturating_sub(valid_len);
250 let keep = valid_len;
251
252 if self.preallocation.length_after_open(keep, dropped, total) < total {
253 if valid_len == 0 {
254 self.vfs
258 .truncate(last, 0)
259 .map_err(io_err("truncate segment", last))?;
260 self.vfs
261 .sync_data(last)
262 .map_err(io_err("sync segment", last))?;
263 self.vfs
264 .remove_file(last)
265 .map_err(io_err("remove empty segment", last))?;
266 self.vfs
267 .sync_dir(&self.dir.clone())
268 .map_err(io_err("sync ledger dir", &self.dir.clone()))?;
269 match prior {
270 Some(segment) => {
271 self.seg_path = segment.path;
272 self.seg_len = segment.len;
273 }
274 None => {
275 self.seg_path = self.dir.join(segment_file_name(Seq::FIRST));
276 self.seg_len = 0;
277 }
278 }
279 } else {
280 self.vfs
281 .truncate(last, keep)
282 .map_err(io_err("truncate segment", last))?;
283 self.vfs
284 .sync_data(last)
285 .map_err(io_err("sync segment", last))?;
286 self.seg_path = last.to_path_buf();
287 self.seg_len = keep;
288 }
289 } else {
290 self.seg_path = last.to_path_buf();
291 self.seg_len = keep;
292 }
293 self.prev = run_prev;
294 self.next_seq = run_seq;
295 Ok((dropped, counted))
296 }
297}
298
299#[derive(Clone, Copy)]
302enum TailProof<'a> {
303 Strict,
305 Records(&'a ProofRecords),
307}
308
309impl TailProof<'_> {
310 fn start(self, vfs: &dyn Vfs, last: &Path, entry: LineCheck) -> std::io::Result<TailStart> {
313 match self {
314 TailProof::Strict => Ok(TailStart::strict(entry)),
315 TailProof::Records(records) => records.tail_start(vfs, last, entry),
316 }
317 }
318}
319
320fn chain_refusal(last: &Path, line: u64) -> StorageError {
321 StorageError::Envelope {
322 path: last.to_path_buf(), line,
323 source: AxError::failure(AxCode::InvalidArgs, "verify chain", "a line does not continue the chain")
324 .with_recovery("run `sprawling replay <ledger-dir>` to see the first line that breaks, then restore that segment from its checkpoint commit"),
325 }
326}
327
328pub(super) fn unversioned(v: u64) -> AxError {
333 AxError::failure(
334 AxCode::InvalidArgs,
335 "read a ledger line's version",
336 format!("v{v} is below the first version any build wrote"),
337 )
338 .with_recovery(
339 "restore this segment from its checkpoint commit: the ledger version starts at 1 \
340 and this line declares less",
341 )
342}
343
344#[cfg(test)]
345#[allow(
346 clippy::unwrap_used,
347 clippy::expect_used,
348 clippy::panic,
349 clippy::indexing_slicing,
350 clippy::wildcard_enum_match_arm,
351 clippy::let_underscore_must_use,
352 clippy::let_underscore_untyped,
353 reason = "test code"
354)]
355mod tests;
356
357#[cfg(test)]
358#[allow(clippy::unwrap_used, reason = "test code")]
359mod preallocated;
360
361#[cfg(test)]
362impl JsonlLedger {
363 pub(crate) fn open_preallocated(
367 fs: crate::fault_fs::FaultFs,
368 dir: &Path,
369 now: TimeMs,
370 ) -> Result<(Self, OpenReport), StorageError> {
371 let (mut ledger, segments) = JsonlLedger::unopened(Box::new(fs), dir)?;
372 ledger.preallocation = super::ledger::SegmentPreallocation::ToRollSize;
373 ledger.resume(&segments, now, TailProof::Strict)
374 }
375}