Skip to main content

storage/jsonl/
open.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Ledger opening: version probes and tail recovery.
7
8use 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    complete_lines, is_segment, records_end, segment_file_name, u64_count,
21};
22use super::verify::{LineCheck, LineFault};
23
24impl JsonlLedger {
25    /// Production entrance: std filesystem underneath, and the ledger
26    /// directory's writer lock, taken before anything is read or
27    /// repaired and held for as long as the returned ledger lives.
28    ///
29    /// # Errors
30    /// `LedgerHeld` when another writer holds this ledger, in this
31    /// process or another; otherwise whatever reading and repairing the
32    /// segments reports.
33    pub fn open(dir: &Path, now: TimeMs) -> Result<(Self, OpenReport), StorageError> {
34        JsonlLedger::open_locked(dir, now, TailProof::Strict)
35    }
36
37    /// [`JsonlLedger::open`], with the last segment's verified-prefix
38    /// record in `records` standing in for checking its prefix line by
39    /// line when the record holds (`crates/storage/spec/Jsonl.lean` §8-34). Records are read,
40    /// never written: the proof that holds the writer lock writes them.
41    ///
42    /// # Errors
43    /// Those of [`JsonlLedger::open`].
44    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    /// The `fault` entrance: the same ledger, over the deterministic
65    /// power-loss model, for a caller that needs one named write to fail.
66    ///
67    /// Takes the concrete adapter rather than the trait, so the `Vfs`
68    /// seam stays inner (`crates/storage/spec/Jsonl.lean` §8-1) and no `pub trait` leaves this
69    /// crate. What comes back is the same `JsonlLedger` production uses -
70    /// a caller above this crate exercises its real code and loses only
71    /// the write it named.
72    ///
73    /// # Errors
74    /// Propagates whatever [`JsonlLedger::open`] would, and the power
75    /// loss itself when the plan cuts during the open.
76    #[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    /// Injection point for the second Vfs adapter, checking the last
86    /// segment line by line; `open_faulty` is its entrance.
87    #[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    /// The ledger at the root of an empty chain, with the segments `dir`
107    /// holds, before anything on them is read.
108    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            // The projection exists only when this ledger is a city's:
129            // it is what tells a writer handed the ledger directory
130            // alone where the buildings' sessions lie. A store opened
131            // directly has no buildings, and files nothing.
132            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    /// Reads the version and the tail of `segments` and puts the writer
142    /// at the end of the surviving records.
143    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    /// Tail-truncation recovery over the last segment, taking its
174    /// verified prefix from `proof` when the record holds (`crates/storage/spec/Jsonl.lean`
175    /// §8-34). Returns dropped bytes and what was read and checked; on
176    /// return the ledger state points at the surviving tail.
177    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 file = self.vfs.read(last).map_err(io_err("read segment", last))?;
186        // Zeros after the last record are preallocated space, not a tear
187        // (`crates/storage/spec/Jsonl/Preallocate.lean`): the scan reads the
188        // segment without them, and the writer resumes where they begin.
189        let filled = records_end(&file);
190        let total = u64_count(file.len()).map_err(io_err("measure segment", last))?;
191        let mut bytes = file;
192        bytes.truncate(filled);
193        let start = proof.start(
194            last,
195            &bytes,
196            LineCheck::after(boundary.prev, boundary.next_seq),
197        );
198        let mut check = start.check;
199        let mut counted = start.counted;
200        let (lines, _terminated_len) = complete_lines(bytes.get(start.from..).unwrap_or_default());
201
202        let mut valid_len = start.from;
203        for (index, line) in lines.iter().enumerate() {
204            let at = u64_count(index)
205                .map_err(io_err("number a segment line", last))?
206                .saturating_add(1)
207                .saturating_add(start.lines);
208            counted.lines_checked = counted.lines_checked.saturating_add(1);
209            let fault = match check.advance(line) {
210                Ok(_) => {
211                    valid_len = valid_len.saturating_add(line.len()).saturating_add(1);
212                    continue;
213                }
214                Err(fault) => fault,
215            };
216            // What a version refuses, it refuses by name: a line this
217            // build cannot read is not tail damage, and truncating it
218            // would delete a newer build's history (`crates/storage/spec/Jsonl.lean` §8-1).
219            // A tear only ever damages the tail and never leaves a record
220            // behind, so a break is refused - not truncated - when the
221            // breaking line carries an envelope (a fork: a second writer
222            // continued the same prev) or intact records still follow it
223            // (non-tail damage). Newline-bearing garbage is no record.
224            // The breaking line itself counts: a record behind a run of
225            // zeros still carries its envelope.
226            let later_intact = || {
227                lines
228                    .iter()
229                    .skip(index)
230                    .any(|l| LineCheck::carries_envelope(l))
231            };
232            let source = match fault {
233                LineFault::VersionAhead(v) => {
234                    return Err(StorageError::VersionAhead {
235                        path: last.to_path_buf(),
236                        v,
237                    });
238                }
239                LineFault::NotAVersion(v) => unversioned(v),
240                LineFault::NotALine(_) if !later_intact() => break,
241                LineFault::ChainBreak | LineFault::SeqGap { .. } | LineFault::NotALine(_) => {
242                    AxError::failure(
243                        AxCode::InvalidArgs,
244                        "verify chain",
245                        "a line does not continue the chain",
246                    )
247                    .with_recovery(
248                        "run `sprawling replay <ledger-dir>` to see the first line \
249                     that breaks, then restore that segment from its \
250                     checkpoint commit",
251                    )
252                }
253                other @ (LineFault::UnknownKind(_)
254                | LineFault::NotCanonical(_)
255                | LineFault::SeqExhausted(_)) => other.into_ax(at),
256            };
257            return Err(StorageError::Envelope {
258                path: last.to_path_buf(),
259                line: at,
260                source,
261            });
262        }
263        let run_prev = check.prev();
264        let run_seq = check.expected();
265
266        let measure = |count: usize| u64_count(count).map_err(io_err("measure segment", last));
267        let dropped = measure(filled.saturating_sub(valid_len))?;
268        let keep = measure(valid_len)?;
269
270        if self.preallocation.length_after_open(keep, dropped, total) < total {
271            if valid_len == 0 {
272                // Whole last segment is torn. Remove it; the tail falls
273                // back to the prior segment (or to an empty city when this
274                // was the only one — the genesis append never returned Ok).
275                self.vfs
276                    .truncate(last, 0)
277                    .map_err(io_err("truncate segment", last))?;
278                self.vfs
279                    .sync_data(last)
280                    .map_err(io_err("sync segment", last))?;
281                self.vfs
282                    .remove_file(last)
283                    .map_err(io_err("remove empty segment", last))?;
284                self.vfs
285                    .sync_dir(&self.dir.clone())
286                    .map_err(io_err("sync ledger dir", &self.dir.clone()))?;
287                match prior {
288                    Some(segment) => {
289                        self.seg_path = segment.path;
290                        self.seg_len = segment.len;
291                    }
292                    None => {
293                        self.seg_path = self.dir.join(segment_file_name(Seq::FIRST));
294                        self.seg_len = 0;
295                    }
296                }
297            } else {
298                self.vfs
299                    .truncate(last, keep)
300                    .map_err(io_err("truncate segment", last))?;
301                self.vfs
302                    .sync_data(last)
303                    .map_err(io_err("sync segment", last))?;
304                self.seg_path = last.to_path_buf();
305                self.seg_len = keep;
306            }
307        } else {
308            self.seg_path = last.to_path_buf();
309            self.seg_len = keep;
310        }
311        self.prev = run_prev;
312        self.next_seq = run_seq;
313        Ok((dropped, counted))
314    }
315}
316
317/// What tail recovery may take in place of checking the last segment's
318/// prefix line by line.
319#[derive(Clone, Copy)]
320enum TailProof<'a> {
321    /// Every line of the last segment is checked.
322    Strict,
323    /// The last segment's record in these, when it holds (`crates/storage/spec/Jsonl.lean` §8-34).
324    Records(&'a ProofRecords),
325}
326
327impl TailProof<'_> {
328    /// Where checking `bytes`, the last segment at `last`, begins when
329    /// the chain state entering it is `entry`.
330    fn start(self, last: &Path, bytes: &[u8], entry: LineCheck) -> TailStart {
331        match (self, last.file_name().and_then(|name| name.to_str())) {
332            (TailProof::Records(records), Some(name)) => records.tail_start(name, bytes, entry),
333            (TailProof::Records(_), None) | (TailProof::Strict, _) => {
334                TailStart::strict(bytes, entry)
335            }
336        }
337    }
338}
339
340/// The refusal a line below the first ledger version earns.
341///
342/// One sentence for both readers, because "nobody ever wrote this" is
343/// one fact whether the probe or the tail scan meets it first.
344pub(super) fn unversioned(v: u64) -> AxError {
345    AxError::failure(
346        AxCode::InvalidArgs,
347        "read a ledger line's version",
348        format!("v{v} is below the first version any build wrote"),
349    )
350    .with_recovery(
351        "restore this segment from its checkpoint commit: the ledger version starts at 1 \
352         and this line declares less",
353    )
354}
355
356#[cfg(test)]
357#[allow(
358    clippy::unwrap_used,
359    clippy::expect_used,
360    clippy::panic,
361    clippy::indexing_slicing,
362    clippy::wildcard_enum_match_arm,
363    clippy::let_underscore_must_use,
364    clippy::let_underscore_untyped,
365    reason = "test code"
366)]
367mod tests;
368
369#[cfg(test)]
370#[allow(clippy::unwrap_used, reason = "test code")]
371mod preallocated;
372
373#[cfg(test)]
374impl JsonlLedger {
375    /// The `fault` entrance with the preallocating arm, whatever
376    /// [`SEGMENT_PREALLOCATION`] says, so its tests judge the arm the
377    /// build does not select (storage D31).
378    pub(crate) fn open_preallocated(
379        fs: crate::fault_fs::FaultFs,
380        dir: &Path,
381        now: TimeMs,
382    ) -> Result<(Self, OpenReport), StorageError> {
383        let (mut ledger, segments) = JsonlLedger::unopened(Box::new(fs), dir)?;
384        ledger.preallocation = super::ledger::SegmentPreallocation::ToRollSize;
385        ledger.resume(&segments, now, TailProof::Strict)
386    }
387}