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