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::{
19 JsonlLedger, WriteObserver, complete_lines, is_segment, segment_file_name, u64_count,
20};
21
22impl JsonlLedger {
23 pub(crate) fn append_log_truncated(
24 &mut self,
25 now: TimeMs,
26 dropped: u64,
27 ) -> Result<(), StorageError> {
28 let data = Payload::of(&kernel::event::record::LogTruncated {
29 dropped_bytes: dropped,
30 })
31 .map_err(|source| StorageError::Draft { source })?;
32 let draft = EventDraft {
33 run: RunId::CITY,
34 t: now,
35 who: kernel::event::Who::City.to_string(),
36 addr: None,
37 kind: EventKind::LogTruncated,
38 data,
39 ig: false,
40 };
41 self.append_all(vec![draft]).map(|_| ())
42 }
43
44 pub fn append_all(&mut self, drafts: Vec<EventDraft>) -> Result<Vec<EventRef>, StorageError> {
47 self.finish_unwind()?;
51 self.barrier.admit(&self.dir, self.next_seq)?;
52 self.halt.admit()?;
53 if drafts.is_empty() {
54 return Ok(Vec::new());
55 }
56 let mut seq = self.next_seq;
57 let mut prev = self.prev;
58 let mut cur_path = self.seg_path.clone();
59 let mut cur_len = self.seg_len;
60 let mut writes: Vec<(PathBuf, u64, Vec<u8>)> = Vec::new();
61 let mut created: Vec<PathBuf> = Vec::new();
62 let mut records = Vec::with_capacity(drafts.len());
63
64 if cur_len == 0 && !self.vfs.exists(&cur_path) {
65 created.push(cur_path.clone());
66 }
67
68 for draft in drafts {
69 let record = EventRecord::from_draft(draft, seq, prev);
70 let line = record
71 .canonical_line()
72 .map_err(|source| StorageError::Draft { source })?;
73 let line_len = u64_count(line.len())
74 .map_err(io_err("measure an event line", &cur_path))?
75 .saturating_add(1);
76 if cur_len > 0 && cur_len.saturating_add(line_len) > self.roll_bytes {
77 cur_path = self.dir.join(segment_file_name(seq));
78 cur_len = 0;
79 created.push(cur_path.clone());
80 }
81 prev = chain_hash(&line);
82 seq = seq
83 .next()
84 .map_err(|source| StorageError::Draft { source })?;
85 records.push(record);
86 let mut terminated = line;
87 terminated.push(b'\n');
88 match writes.last_mut() {
89 Some((path, _, buffer)) if *path == cur_path => {
90 buffer.extend_from_slice(&terminated);
91 }
92 _ => writes.push((cur_path.clone(), cur_len, terminated)),
93 }
94 cur_len = cur_len.saturating_add(line_len);
95 }
96
97 self.barrier = Barrier::Broken;
98 self.write_wave(&writes, created)?;
99
100 self.barrier = Barrier::Whole;
101 self.seg_path = cur_path;
102 self.seg_len = cur_len;
103 self.next_seq = seq;
104 self.prev = prev;
105
106 if let Some(sessions) = self.sessions.as_mut() {
112 for record in &records {
113 if let Err(error) = sessions.absorb(record) {
114 eprintln!("a session slice was refused and skipped: {error}");
115 }
116 }
117 }
118
119 if let Some(observer) = self.observer.as_mut() {
123 for record in &records {
124 observer(record);
125 }
126 }
127 Ok(records.iter().map(EventRecord::to_ref).collect())
128 }
129
130 pub fn hand_off_session_slices(&mut self) -> Option<crate::sessions::Sessions> {
135 self.sessions.take()
136 }
137
138 #[must_use]
145 pub fn position(&self) -> Seq {
146 self.next_seq
147 }
148
149 pub fn observe(&mut self, sink: WriteObserver) {
153 self.observer = Some(sink);
154 }
155
156 pub fn read_raw_lines(&self) -> Result<Vec<Vec<u8>>, StorageError> {
159 let mut out = Vec::new();
160 let segments: Vec<PathBuf> = self
161 .vfs
162 .list(&self.dir)
163 .map_err(io_err("list ledger dir", &self.dir))?
164 .into_iter()
165 .filter(|p| is_segment(p))
166 .collect();
167 for seg in segments {
168 let bytes = self.vfs.read(&seg).map_err(io_err("read segment", &seg))?;
169 let (lines, _) = complete_lines(&bytes);
170 for line in lines {
171 if !line.is_empty() {
172 out.push(line.to_vec());
173 }
174 }
175 }
176 Ok(out)
177 }
178
179 #[cfg(test)]
180 pub(crate) fn set_roll_bytes_for_test(&mut self, roll_bytes: u64) {
181 self.roll_bytes = roll_bytes;
182 }
183}
184
185impl kernel::Ledger for JsonlLedger {
186 fn append(&mut self, draft: EventDraft) -> Result<EventRef, AxError> {
187 let refs = self
188 .append_all(vec![draft])
189 .map_err(StorageError::into_ax)?;
190 refs.into_iter().next().ok_or_else(|| {
191 AxError::failure(AxCode::InvalidArgs, "append event", "empty wave echo").with_recovery(
192 "report this against storage::jsonl::append with the city path: \
193 append_all owes one echo per draft and returned none",
194 )
195 })
196 }
197
198 fn append_all(&mut self, drafts: Vec<EventDraft>) -> Result<Vec<EventRef>, AxError> {
200 JsonlLedger::append_all(self, drafts).map_err(StorageError::into_ax)
201 }
202}
203
204#[cfg(any(test, feature = "conformance"))]
205impl kernel::ledger::conformance::LedgerInspect for JsonlLedger {
206 fn raw_lines(&self) -> Result<Vec<Vec<u8>>, AxError> {
207 self.read_raw_lines().map_err(StorageError::into_ax)
208 }
209}
210
211#[cfg(test)]
212#[allow(
213 clippy::unwrap_used,
214 clippy::expect_used,
215 clippy::panic,
216 clippy::indexing_slicing,
217 reason = "test code"
218)]
219mod tests {
220 use super::*;
221 use kernel::GENESIS_PREV;
222 use std::fs;
223 use std::path::Path;
224 fn draft(kind: EventKind, t: u64) -> EventDraft {
225 EventDraft {
226 run: RunId::CITY,
227 t: TimeMs::new(t),
228 who: "city".to_string(),
229 addr: None,
230 kind,
231 data: Payload::empty(),
232 ig: false,
233 }
234 }
235 fn verify_chain(lines: &[Vec<u8>]) {
236 let mut prev = GENESIS_PREV;
237 for (i, line) in lines.iter().enumerate() {
238 let record = EventRecord::parse_line(line).unwrap();
239 assert_eq!(record.prev(), prev, "prev broken at line {i}");
240 assert_eq!(record.seq(), Seq::new(u64::try_from(i).unwrap()));
241 prev = chain_hash(line);
242 }
243 }
244
245 #[test]
246 fn passes_the_kernel_conformance_suite() {
247 use kernel::ledger::conformance::assert_ledger_conformance;
248 let keep: std::cell::RefCell<Vec<tempfile::TempDir>> = std::cell::RefCell::new(Vec::new());
249 assert_ledger_conformance(|| {
250 let dir = tempfile::tempdir().unwrap();
251 let (ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
252 keep.borrow_mut().push(dir);
253 ledger
254 });
255 }
256
257 #[test]
258 fn rolls_segments_without_breaking_seq_or_chain() {
259 let dir = tempfile::tempdir().unwrap();
260 let (mut ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
261 ledger.set_roll_bytes_for_test(1);
262 for t in 0..4 {
263 ledger
264 .append_all(vec![draft(EventKind::GateChecked, t)])
265 .unwrap();
266 }
267 let segments = fs::read_dir(dir.path()).unwrap().count();
268 assert!(segments >= 4, "tiny roll budget must produce many segments");
269 let lines = ledger.read_raw_lines().unwrap();
270 assert_eq!(lines.len(), 4);
271 verify_chain(&lines);
272
273 drop(ledger);
274 let (mut reopened, report) = JsonlLedger::open(dir.path(), TimeMs::new(9)).unwrap();
275 assert!(report.recovered.is_none());
276 reopened
277 .append_all(vec![draft(EventKind::RunFrozen, 9)])
278 .unwrap();
279 verify_chain(&reopened.read_raw_lines().unwrap());
280 }
281
282 #[test]
287 fn one_append_costs_a_fixed_number_of_disk_operations() {
288 let dir = tempfile::tempdir().unwrap();
289 let fs = crate::fault_fs::FaultFs::new(crate::fault_fs::FaultPlan {
290 cut_at_op: None,
291 cut_on_write: None,
292 torn_tail: crate::fault_fs::TornTail::None,
293 });
294 let (mut ledger, _) =
295 JsonlLedger::open_faulty(fs.clone(), dir.path(), TimeMs::new(0)).unwrap();
296 let opened = fs.op_count();
297 kernel::Ledger::append(&mut ledger, draft(EventKind::GateChecked, 1)).unwrap();
298 let first = fs.op_count().saturating_sub(opened);
299 kernel::Ledger::append(&mut ledger, draft(EventKind::GateChecked, 2)).unwrap();
300 let second = fs.op_count().saturating_sub(opened).saturating_sub(first);
301 let bytes: u64 = ledger
302 .read_raw_lines()
303 .unwrap()
304 .iter()
305 .map(|line| u64::try_from(line.len().saturating_add(1)).unwrap())
306 .sum();
307 eprintln!(
308 "ledger_append_ops: {first} disk ops for the first record, {second} for \n every one after it, {bytes} B on disk for two records"
309 );
310 assert!(
311 first > second,
312 "the first record also creates the segment and syncs its directory entry"
313 );
314 }
315
316 fn only_segment(dir: &Path) -> std::path::PathBuf {
317 let mut files: Vec<_> = fs::read_dir(dir)
318 .unwrap()
319 .map(|e| e.unwrap().path())
320 .collect();
321 files.sort();
322 assert_eq!(files.len(), 1);
323 files.remove(0)
324 }
325
326 fn faulty(dir: &Path) -> (JsonlLedger, crate::fault_fs::FaultFs) {
327 let fs = crate::fault_fs::FaultFs::new(crate::fault_fs::FaultPlan {
328 cut_at_op: None,
329 cut_on_write: None,
330 torn_tail: crate::fault_fs::TornTail::None,
331 });
332 let (ledger, _) = JsonlLedger::open_faulty(fs.clone(), dir, TimeMs::new(0)).unwrap();
333 (ledger, fs)
334 }
335
336 fn drafts(count: u64) -> Vec<EventDraft> {
337 (0..count)
338 .map(|t| draft(EventKind::GateChecked, t))
339 .collect()
340 }
341
342 #[test]
345 fn a_wave_costs_one_write_and_one_barrier_whatever_it_carries() {
346 let dir = tempfile::tempdir().unwrap();
347 let (mut ledger, fs) = faulty(dir.path());
348 ledger.append_all(drafts(1)).unwrap();
349 let before_one = fs.op_count();
350 ledger.append_all(drafts(1)).unwrap();
351 let one = fs.op_count() - before_one;
352 let before_three = fs.op_count();
353 ledger.append_all(drafts(3)).unwrap();
354 let three = fs.op_count() - before_three;
355 assert_eq!(three, one);
356 }
357
358 #[test]
361 fn a_wave_across_the_roll_puts_each_line_in_the_segment_named_for_it() {
362 let dir = tempfile::tempdir().unwrap();
363 let (mut ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
364 ledger.set_roll_bytes_for_test(1);
365 ledger.append_all(drafts(3)).unwrap();
366 let mut held: Vec<(String, Vec<u64>)> = fs::read_dir(dir.path())
367 .unwrap()
368 .map(|entry| entry.unwrap().path())
369 .filter(|path| is_segment(path))
370 .map(|path| {
371 let seqs = fs::read(&path)
372 .unwrap()
373 .split(|byte| *byte == b'\n')
374 .filter(|line| !line.is_empty())
375 .map(|line| EventRecord::parse_line(line).unwrap().seq().value())
376 .collect();
377 (
378 path.file_name().unwrap().to_string_lossy().into_owned(),
379 seqs,
380 )
381 })
382 .collect();
383 held.sort();
384 let expected: Vec<(String, Vec<u64>)> = (0..3)
385 .map(|seq| (segment_file_name(Seq::new(seq)), vec![seq]))
386 .collect();
387 assert_eq!(held, expected);
388 }
389
390 #[test]
393 fn a_line_that_fills_the_segment_exactly_does_not_roll() {
394 let dir = tempfile::tempdir().unwrap();
395 let (mut ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
396 ledger.append_all(drafts(2)).unwrap();
397 let exact = u64::try_from(fs::read(only_segment(dir.path())).unwrap().len()).unwrap();
398 let fresh = tempfile::tempdir().unwrap();
399 let (mut ledger, _) = JsonlLedger::open(fresh.path(), TimeMs::new(0)).unwrap();
400 ledger.set_roll_bytes_for_test(exact);
401 ledger.append_all(drafts(1)).unwrap();
402 ledger.append_all(drafts(1)).unwrap();
403 assert_eq!(
404 fs::read(only_segment(fresh.path())).unwrap().len(),
405 usize::try_from(exact).unwrap()
406 );
407 }
408
409 #[test]
412 fn the_observer_hears_each_record_of_a_wave_in_order() {
413 let dir = tempfile::tempdir().unwrap();
414 let (mut ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
415 let heard = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
416 let sink = std::sync::Arc::clone(&heard);
417 ledger.observe(Box::new(move |record| {
418 sink.lock().unwrap().push(record.seq().value());
419 }));
420 ledger.append_all(drafts(3)).unwrap();
421 assert_eq!(*heard.lock().unwrap(), vec![0, 1, 2]);
422 }
423
424 #[test]
427 fn the_port_s_batch_append_echoes_what_it_wrote() {
428 let dir = tempfile::tempdir().unwrap();
429 let (mut ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
430 let echoed = kernel::Ledger::append_all(&mut ledger, drafts(2)).unwrap();
431 let written: Vec<_> = ledger
432 .read_raw_lines()
433 .unwrap()
434 .iter()
435 .map(|line| EventRecord::parse_line(line).unwrap().to_ref())
436 .collect();
437 assert_eq!(echoed, written);
438 }
439
440 #[test]
444 fn an_empty_segment_a_crash_left_is_appended_to_without_a_directory_barrier() {
445 let dir = tempfile::tempdir().unwrap();
446 let fs = crate::fault_fs::FaultFs::new(crate::fault_fs::FaultPlan {
447 cut_at_op: None,
448 cut_on_write: None,
449 torn_tail: crate::fault_fs::TornTail::None,
450 });
451 let mut seeded = fs.clone();
454 crate::vfs::Vfs::create_dir_all(&mut seeded, dir.path()).unwrap();
455 crate::vfs::Vfs::append(
456 &mut seeded,
457 &dir.path().join(segment_file_name(Seq::FIRST)),
458 b"",
459 )
460 .unwrap();
461 let (mut ledger, _) =
462 JsonlLedger::open_faulty(fs.clone(), dir.path(), TimeMs::new(0)).unwrap();
463 let before_first = fs.op_count();
464 ledger.append_all(drafts(1)).unwrap();
465 let first = fs.op_count() - before_first;
466 let before_second = fs.op_count();
467 ledger.append_all(drafts(1)).unwrap();
468 let second = fs.op_count() - before_second;
469 assert_eq!(first, second);
470 }
471
472 proptest::proptest! {
473 #[test]
474 fn any_tail_damage_recovers_to_a_valid_chain(
475 n in 1usize..5,
476 cut_back in 1usize..40,
477 garbage in proptest::collection::vec(1u8..=255, 0..24),
478 ) {
479 let dir = tempfile::tempdir().unwrap();
480 let (mut ledger, _) = JsonlLedger::open(dir.path(), TimeMs::new(0)).unwrap();
481 let drafts: Vec<_> = (0..n)
482 .map(|i| draft(EventKind::GateChecked, u64::try_from(i).unwrap()))
483 .collect();
484 ledger.append_all(drafts).unwrap();
485 let baseline = ledger.read_raw_lines().unwrap();
486 let seg = only_segment(dir.path());
487 drop(ledger);
488
489 let mut bytes = fs::read(&seg).unwrap();
490 let keep = bytes.len().saturating_sub(cut_back);
491 bytes.truncate(keep);
492 bytes.extend_from_slice(&garbage);
493 fs::write(&seg, &bytes).unwrap();
494
495 let (reopened, _) = JsonlLedger::open(dir.path(), TimeMs::new(999)).unwrap();
496 let lines = reopened.read_raw_lines().unwrap();
497 verify_chain(&lines);
498 let survivors = lines
500 .iter()
501 .filter(|l| {
502 EventRecord::parse_line(l).unwrap().kind() != EventKind::LogTruncated
503 })
504 .count();
505 proptest::prop_assert!(survivors <= baseline.len());
506 for (mine, original) in lines.iter().take(survivors).zip(baseline.iter()) {
507 proptest::prop_assert_eq!(mine, original);
508 }
509 }
510 }
511}