storage/snapshot/
start.rs1use std::path::Path;
11
12use kernel::Seq;
13
14use super::{ChainSnapshot, SnapshotFit, StoredSnapshot, read_snapshot};
15use crate::error::{StorageError, io_err};
16use crate::jsonl::{claimed_seq, complete_lines, ledger_segments_at, segment_first_seq};
17use crate::real_fs::RealFs;
18use crate::vfs::Vfs;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum SnapshotStart {
24 Resume {
26 snapshot: ChainSnapshot,
27 tail: Vec<Vec<u8>>,
28 },
29 Whole(WholeFold),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum WholeFold {
37 NoSnapshot,
38 Damaged(String),
40 OtherFoldVersion {
42 found: u32,
43 },
44 Stale,
46 Missing,
48}
49
50pub fn start_from_snapshot(
59 ledger_dir: &Path,
60 snapshot_dir: &Path,
61 fold_version: u32,
62) -> Result<SnapshotStart, StorageError> {
63 start_through(&RealFs::new(), ledger_dir, snapshot_dir, fold_version)
64}
65
66pub(crate) fn start_through(
69 vfs: &dyn Vfs,
70 ledger_dir: &Path,
71 snapshot_dir: &Path,
72 fold_version: u32,
73) -> Result<SnapshotStart, StorageError> {
74 let because = match read_snapshot(snapshot_dir)? {
75 StoredSnapshot::Absent => WholeFold::NoSnapshot,
76 StoredSnapshot::Damaged(reason) => WholeFold::Damaged(reason),
77 StoredSnapshot::Present(snapshot) if snapshot.fold_version() != fold_version => {
78 WholeFold::OtherFoldVersion {
79 found: snapshot.fold_version(),
80 }
81 }
82 StoredSnapshot::Present(snapshot) => return tail_through(vfs, ledger_dir, snapshot),
83 };
84 Ok(SnapshotStart::Whole(because))
85}
86
87pub fn tail_after(
94 ledger_dir: &Path,
95 snapshot: ChainSnapshot,
96) -> Result<SnapshotStart, StorageError> {
97 tail_through(&RealFs::new(), ledger_dir, snapshot)
98}
99
100fn tail_through(
101 vfs: &dyn Vfs,
102 ledger_dir: &Path,
103 snapshot: ChainSnapshot,
104) -> Result<SnapshotStart, StorageError> {
105 let because = match lines_from_cut(vfs, ledger_dir, snapshot.seq())? {
106 None => WholeFold::Missing,
107 Some(Cut { line_at_seq, tail }) => match snapshot.fit(&line_at_seq) {
108 SnapshotFit::Fits => return Ok(SnapshotStart::Resume { snapshot, tail }),
109 SnapshotFit::Stale => WholeFold::Stale,
110 },
111 };
112 Ok(SnapshotStart::Whole(because))
113}
114
115struct Cut {
117 line_at_seq: Vec<u8>,
118 tail: Vec<Vec<u8>>,
119}
120
121fn lines_from_cut(vfs: &dyn Vfs, dir: &Path, seq: Seq) -> Result<Option<Cut>, StorageError> {
126 let segments = ledger_segments_at(dir)?;
127 let Some((at, first)) = segments.iter().enumerate().rev().find_map(|(at, segment)| {
128 first_seq_of(segment)
129 .filter(|first| *first <= seq)
130 .map(|first| (at, first))
131 }) else {
132 return Ok(None);
133 };
134 let mut onward = segments.iter().skip(at);
135 let Some(holding) = onward.next() else {
136 return Ok(None);
137 };
138 let bytes = vfs.read(holding).map_err(io_err("read segment", holding))?;
139 let Some(mut cut) = from_the_end(&bytes, seq).or_else(|| from_the_start(&bytes, first, seq))
140 else {
141 return Ok(None);
142 };
143 for segment in onward {
144 let bytes = vfs.read(segment).map_err(io_err("read segment", segment))?;
145 cut.tail.extend(
146 complete_lines(&bytes)
147 .0
148 .into_iter()
149 .filter(|line| !line.is_empty())
150 .map(<[u8]>::to_vec),
151 );
152 }
153 Ok(Some(cut))
154}
155
156fn from_the_end(bytes: &[u8], seq: Seq) -> Option<Cut> {
162 let end = bytes.iter().rposition(|byte| *byte == b'\n')?;
163 let mut lines = bytes
164 .get(..end)?
165 .rsplit(|byte| *byte == b'\n')
166 .filter(|line| !line.is_empty());
167 let newest = lines.clone().next()?;
168 let back = claimed_seq(newest)?.value().checked_sub(seq.value())?;
169 let back = usize::try_from(back).ok()?;
170 let mut tail: Vec<Vec<u8>> = lines.by_ref().take(back).map(<[u8]>::to_vec).collect();
171 if tail.len() != back {
172 return None;
173 }
174 let line_at_seq = lines.next()?.to_vec();
175 tail.reverse();
176 Some(Cut { line_at_seq, tail })
177}
178
179fn from_the_start(bytes: &[u8], first: Seq, seq: Seq) -> Option<Cut> {
183 let skip = usize::try_from(seq.value().checked_sub(first.value())?).ok()?;
185 let mut lines = complete_lines(bytes)
186 .0
187 .into_iter()
188 .filter(|line| !line.is_empty())
189 .skip(skip);
190 let line_at_seq = lines.next()?.to_vec();
191 let tail = lines.map(<[u8]>::to_vec).collect();
192 Some(Cut { line_at_seq, tail })
193}
194
195fn first_seq_of(segment: &Path) -> Option<Seq> {
196 segment
197 .file_name()
198 .and_then(|name| name.to_str())
199 .and_then(segment_first_seq)
200}
201
202#[cfg(test)]
203#[allow(
204 clippy::unwrap_used,
205 clippy::indexing_slicing,
206 clippy::arithmetic_side_effects,
207 clippy::as_conversions,
208 reason = "test code"
209)]
210mod tests;