1use std::path::PathBuf;
9
10use kernel::ledger::chain_hash;
11use kernel::{
12 AxCode, AxError, EventDraft, EventKind, EventRecord, EventRef, Payload, RunId, Seq, TimeMs,
13};
14
15use crate::error::{StorageError, io_err};
16
17use super::barrier::Barrier;
18use super::ledger::{JsonlLedger, WriteObserver, complete_lines, is_segment, segment_file_name};
19
20impl JsonlLedger {
21 pub(crate) fn append_log_truncated(
22 &mut self,
23 now: TimeMs,
24 dropped: u64,
25 ) -> Result<(), StorageError> {
26 let data = Payload::of(&kernel::event::record::LogTruncated {
27 dropped_bytes: dropped,
28 })
29 .map_err(|source| StorageError::Draft { source })?;
30 let draft = EventDraft {
31 run: RunId::CITY,
32 t: now,
33 who: kernel::event::Who::City.to_string(),
34 addr: None,
35 kind: EventKind::LogTruncated,
36 data,
37 ig: false,
38 };
39 self.append_all(vec![draft]).map(|_| ())
40 }
41
42 pub fn append_all(&mut self, drafts: Vec<EventDraft>) -> Result<Vec<EventRef>, StorageError> {
45 self.finish_unwind()?;
49 self.barrier.admit(&self.dir, self.next_seq)?;
50 self.halt.admit()?;
51 if drafts.is_empty() {
52 return Ok(Vec::new());
53 }
54 let mut seq = self.next_seq;
55 let mut prev = self.prev;
56 let mut cur_path = self.seg_path.clone();
57 let mut cur_len = self.seg_len;
58 let mut writes: Vec<(PathBuf, Vec<u8>)> = Vec::new();
59 let mut created: Vec<PathBuf> = Vec::new();
60 let mut records = Vec::with_capacity(drafts.len());
61
62 if cur_len == 0 && !self.vfs.exists(&cur_path) {
63 created.push(cur_path.clone());
64 }
65
66 for draft in drafts {
67 let record = EventRecord::from_draft(draft, seq, prev);
68 let line = record
69 .canonical_line()
70 .map_err(|source| StorageError::Draft { source })?;
71 let line_len = u64::try_from(line.len())
72 .unwrap_or(u64::MAX)
73 .saturating_add(1);
74 if cur_len > 0 && cur_len.saturating_add(line_len) > self.roll_bytes {
75 cur_path = self.dir.join(segment_file_name(seq));
76 cur_len = 0;
77 created.push(cur_path.clone());
78 }
79 prev = chain_hash(&line);
80 seq = seq
81 .next()
82 .map_err(|source| StorageError::Draft { source })?;
83 records.push(record);
84 let mut terminated = line;
85 terminated.push(b'\n');
86 match writes.last_mut() {
87 Some((path, buffer)) if *path == cur_path => buffer.extend_from_slice(&terminated),
88 _ => writes.push((cur_path.clone(), terminated)),
89 }
90 cur_len = cur_len.saturating_add(line_len);
91 }
92
93 self.barrier = Barrier::Broken;
94 self.write_wave(&writes, created)?;
95
96 self.barrier = Barrier::Whole;
97 self.seg_path = cur_path;
98 self.seg_len = cur_len;
99 self.next_seq = seq;
100 self.prev = prev;
101
102 if let Some(sessions) = self.sessions.as_mut() {
108 for record in &records {
109 if let Err(error) = sessions.absorb(record) {
110 eprintln!("a session slice was refused and skipped: {error}");
111 }
112 }
113 }
114
115 if let Some(observer) = self.observer.as_mut() {
119 for record in &records {
120 observer(record);
121 }
122 }
123 Ok(records.iter().map(EventRecord::to_ref).collect())
124 }
125
126 pub fn hand_off_session_slices(&mut self) -> Option<crate::sessions::Sessions> {
131 self.sessions.take()
132 }
133
134 #[must_use]
141 pub fn position(&self) -> Seq {
142 self.next_seq
143 }
144
145 pub fn observe(&mut self, sink: WriteObserver) {
149 self.observer = Some(sink);
150 }
151
152 pub fn read_raw_lines(&self) -> Result<Vec<Vec<u8>>, StorageError> {
155 let mut out = Vec::new();
156 let segments: Vec<PathBuf> = self
157 .vfs
158 .list(&self.dir)
159 .map_err(io_err("list ledger dir", &self.dir))?
160 .into_iter()
161 .filter(|p| is_segment(p))
162 .collect();
163 for seg in segments {
164 let bytes = self.vfs.read(&seg).map_err(io_err("read segment", &seg))?;
165 let (lines, _) = complete_lines(&bytes);
166 for line in lines {
167 if !line.is_empty() {
168 out.push(line.to_vec());
169 }
170 }
171 }
172 Ok(out)
173 }
174
175 #[cfg(test)]
176 pub(crate) fn set_roll_bytes_for_test(&mut self, roll_bytes: u64) {
177 self.roll_bytes = roll_bytes;
178 }
179}
180
181impl kernel::Ledger for JsonlLedger {
182 fn append(&mut self, draft: EventDraft) -> Result<EventRef, AxError> {
183 let refs = self
184 .append_all(vec![draft])
185 .map_err(StorageError::into_ax)?;
186 refs.into_iter().next().ok_or_else(|| {
187 AxError::failure(AxCode::InvalidArgs, "append event", "empty wave echo").with_recovery(
188 "report this against storage::jsonl::append with the city path: \
189 append_all owes one echo per draft and returned none",
190 )
191 })
192 }
193
194 fn append_all(&mut self, drafts: Vec<EventDraft>) -> Result<Vec<EventRef>, AxError> {
196 JsonlLedger::append_all(self, drafts).map_err(StorageError::into_ax)
197 }
198}
199
200#[cfg(feature = "conformance")]
201impl kernel::ledger::conformance::LedgerInspect for JsonlLedger {
202 fn raw_lines(&self) -> Result<Vec<Vec<u8>>, AxError> {
203 self.read_raw_lines().map_err(StorageError::into_ax)
204 }
205}
206
207#[cfg(test)]
208#[allow(
209 clippy::unwrap_used,
210 clippy::expect_used,
211 clippy::panic,
212 clippy::indexing_slicing,
213 reason = "test code"
214)]
215mod tests {
216 use super::*;
217 use kernel::GENESIS_PREV;
218 use std::fs;
219 use std::path::Path;
220 fn draft(kind: EventKind, t: u64) -> EventDraft {
221 EventDraft {
222 run: RunId::CITY,
223 t: TimeMs::new(t),
224 who: "city".to_string(),
225 addr: None,
226 kind,
227 data: Payload::empty(),
228 ig: false,
229 }
230 }
231 fn verify_chain(lines: &[Vec<u8>]) {
232 let mut prev = GENESIS_PREV;
233 for (i, line) in lines.iter().enumerate() {
234 let record = EventRecord::parse_line(line).unwrap();
235 assert_eq!(record.prev(), prev, "prev broken at line {i}");
236 assert_eq!(record.seq(), Seq::new(u64::try_from(i).unwrap()));
237 prev = chain_hash(line);
238 }
239 }
240
241 #[cfg(feature = "conformance")]
242 #[test]
243 fn passes_the_kernel_conformance_suite() {
244 use kernel::ledger::conformance::assert_ledger_conformance;
245 let keep: std::cell::RefCell<Vec<tempfile::TempDir>> = std::cell::RefCell::new(Vec::new());
246 assert_ledger_conformance(|| {
247 let dir = tempfile::tempdir().unwrap();
248 let (ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
249 keep.borrow_mut().push(dir);
250 ledger
251 });
252 }
253
254 #[test]
255 fn rolls_segments_without_breaking_seq_or_chain() {
256 let dir = tempfile::tempdir().unwrap();
257 let (mut ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
258 ledger.set_roll_bytes_for_test(1);
259 for t in 0..4 {
260 ledger
261 .append_all(vec![draft(EventKind::GateChecked, t)])
262 .unwrap();
263 }
264 let segments = fs::read_dir(dir.path()).unwrap().count();
265 assert!(segments >= 4, "tiny roll budget must produce many segments");
266 let lines = ledger.read_raw_lines().unwrap();
267 assert_eq!(lines.len(), 4);
268 verify_chain(&lines);
269
270 drop(ledger);
271 let (mut reopened, report) = JsonlLedger::open(dir.path(), TimeMs::new(9)).unwrap();
272 assert!(report.recovered.is_none());
273 reopened
274 .append_all(vec![draft(EventKind::RunFrozen, 9)])
275 .unwrap();
276 verify_chain(&reopened.read_raw_lines().unwrap());
277 }
278
279 #[test]
284 fn one_append_costs_a_fixed_number_of_disk_operations() {
285 let dir = tempfile::tempdir().unwrap();
286 let fs = crate::fault_fs::FaultFs::new(crate::fault_fs::FaultPlan {
287 cut_at_op: None,
288 cut_on_write: None,
289 torn_tail: crate::fault_fs::TornTail::None,
290 });
291 let (mut ledger, _) =
292 JsonlLedger::open_faulty(fs.clone(), dir.path(), TimeMs::new(0)).unwrap();
293 let opened = fs.op_count();
294 kernel::Ledger::append(&mut ledger, draft(EventKind::GateChecked, 1)).unwrap();
295 let first = fs.op_count().saturating_sub(opened);
296 kernel::Ledger::append(&mut ledger, draft(EventKind::GateChecked, 2)).unwrap();
297 let second = fs.op_count().saturating_sub(opened).saturating_sub(first);
298 let bytes: u64 = ledger
299 .read_raw_lines()
300 .unwrap()
301 .iter()
302 .map(|line| u64::try_from(line.len().saturating_add(1)).unwrap())
303 .sum();
304 eprintln!(
305 "ledger_append_ops: {first} disk ops for the first record, {second} for \n every one after it, {bytes} B on disk for two records"
306 );
307 assert!(
308 first > second,
309 "the first record also creates the segment and syncs its directory entry"
310 );
311 }
312
313 fn only_segment(dir: &Path) -> std::path::PathBuf {
314 let mut files: Vec<_> = fs::read_dir(dir)
315 .unwrap()
316 .map(|e| e.unwrap().path())
317 .collect();
318 files.sort();
319 assert_eq!(files.len(), 1);
320 files.remove(0)
321 }
322
323 proptest::proptest! {
324 #[test]
325 fn any_tail_damage_recovers_to_a_valid_chain(
326 n in 1usize..5,
327 cut_back in 1usize..40,
328 garbage in proptest::collection::vec(1u8..=255, 0..24),
329 ) {
330 let dir = tempfile::tempdir().unwrap();
331 let (mut ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
332 let drafts: Vec<_> = (0..n)
333 .map(|i| draft(EventKind::GateChecked, u64::try_from(i).unwrap()))
334 .collect();
335 ledger.append_all(drafts).unwrap();
336 let baseline = ledger.read_raw_lines().unwrap();
337 let seg = only_segment(dir.path());
338 drop(ledger);
339
340 let mut bytes = fs::read(&seg).unwrap();
341 let keep = bytes.len().saturating_sub(cut_back);
342 bytes.truncate(keep);
343 bytes.extend_from_slice(&garbage);
344 fs::write(&seg, &bytes).unwrap();
345
346 let (reopened, _) = JsonlLedger::open(dir.path(), TimeMs::new(999)).unwrap();
347 let lines = reopened.read_raw_lines().unwrap();
348 verify_chain(&lines);
349 let survivors = lines
351 .iter()
352 .filter(|l| {
353 EventRecord::parse_line(l).unwrap().kind() != EventKind::LogTruncated
354 })
355 .count();
356 proptest::prop_assert!(survivors <= baseline.len());
357 for (mine, original) in lines.iter().take(survivors).zip(baseline.iter()) {
358 proptest::prop_assert_eq!(mine, original);
359 }
360 }
361 }
362}