1use crate::chain::{self, Follows, Link, Verdict};
31use crate::ledger::{Entry, InterlockEntry, Ledger, LedgerError, Record};
32use crate::store::Store;
33use planarian_schema::{GenerationId, Hash, Timestamp, Transition, TxnId};
34use std::io;
35
36const HEADER: usize = 16;
37const PREV: usize = 32;
38const FORMAT: &[u8] = b"planarian-journal-2";
40
41#[derive(Debug)]
42pub enum JournalError {
43 Io(io::Error),
44 Full,
46 Ledger(LedgerError),
47 Corrupt(String),
48 Integrity(String),
51 Legacy,
53}
54
55impl From<io::Error> for JournalError {
56 fn from(e: io::Error) -> Self {
57 if e.raw_os_error() == Some(28) { JournalError::Full } else { JournalError::Io(e) }
58 }
59}
60
61impl From<LedgerError> for JournalError {
62 fn from(e: LedgerError) -> Self {
63 JournalError::Ledger(e)
64 }
65}
66
67pub struct Locked<'a, S: Store>(&'a mut Journal<S>);
69
70impl<S: Store> Locked<'_, S> {
71 pub fn ledger(&self) -> &Ledger {
72 &self.0.ledger
73 }
74
75 pub fn store(&self) -> &S {
76 &self.0.store
77 }
78
79 pub fn append(&mut self, at: Timestamp, record: Record) -> Result<Entry, JournalError> {
80 self.0.append_locked(at, record)
81 }
82}
83
84pub struct Journal<S: Store> {
85 store: S,
86 ledger: Ledger,
87 offset: u64,
89 checkpoint_seq: u64,
91 head: Hash,
93 pending_sentinel: Option<(TxnId, Vec<u8>)>,
97}
98
99pub fn chain(prev: &Hash, seq: u64, payload: &[u8]) -> Hash {
101 let mut b = Vec::with_capacity(PREV + 8 + payload.len());
102 b.extend_from_slice(&prev.0);
103 b.extend_from_slice(&seq.to_le_bytes());
104 b.extend_from_slice(payload);
105 Hash::of(&b)
106}
107
108fn crc(body: &[u8]) -> u32 {
109 #[cfg(test)]
110 tests::CRC_BYTES.with(|c| c.set(c.get() + body.len() as u64));
111 crc32c::crc32c_append(crc32c::crc32c(FORMAT), body)
112}
113
114fn frame(seq: u64, prev: &Hash, payload: &[u8]) -> Vec<u8> {
115 let mut body = Vec::with_capacity(8 + PREV + payload.len());
116 body.extend_from_slice(&seq.to_le_bytes());
117 body.extend_from_slice(&prev.0);
118 body.extend_from_slice(payload);
119 let mut out = Vec::with_capacity(HEADER + PREV + payload.len());
120 out.extend_from_slice(&((PREV + payload.len()) as u32).to_le_bytes());
121 out.extend_from_slice(&crc(&body).to_le_bytes());
122 out.extend_from_slice(&body);
123 out
124}
125
126struct Frame<'a> {
128 seq: u64,
129 prev: Hash,
130 payload: &'a [u8],
131 size: usize,
132}
133
134fn unframe(bytes: &[u8]) -> Option<Frame<'_>> {
136 if bytes.len() < HEADER {
137 return None;
138 }
139 let len = u32::from_le_bytes(bytes[0..4].try_into().ok()?) as usize;
140 let sum = u32::from_le_bytes(bytes[4..8].try_into().ok()?);
141 if len < PREV || bytes.len() < HEADER + len {
142 return None;
143 }
144 let body = &bytes[8..HEADER + len];
145 if crc(body) != sum {
146 return None;
147 }
148 let seq = u64::from_le_bytes(body[0..8].try_into().ok()?);
149 let prev = Hash(body[8..8 + PREV].try_into().ok()?);
150 Some(Frame { seq, prev, payload: &body[8 + PREV..], size: HEADER + len })
151}
152
153fn could_follow(bytes: &[u8], last_seq: u64, capacity: u64) -> bool {
165 if bytes.len() < HEADER {
166 return false;
167 }
168 let len = u32::from_le_bytes(bytes[0..4].try_into().expect("4 bytes")) as usize;
169 let seq = u64::from_le_bytes(bytes[8..16].try_into().expect("8 bytes"));
170 let most = capacity / (HEADER + PREV) as u64;
171 if len < PREV || bytes.len() < HEADER + len || seq <= last_seq || seq - last_seq > most {
172 return false;
173 }
174 let prefix = format!("{{\"seq\":{seq},");
175 bytes[HEADER + PREV..HEADER + len].starts_with(prefix.as_bytes())
176}
177
178pub fn tamper<S: Store>(store: &S, nth: usize, edit: impl FnOnce(&mut Vec<u8>)) -> io::Result<bool> {
184 let mut off = 0u64;
185 for i in 0.. {
186 let header = store.read_at(off, HEADER)?;
187 if header.len() < HEADER {
188 return Ok(false);
189 }
190 let len = u32::from_le_bytes(header[0..4].try_into().expect("4 bytes")) as usize;
191 let bytes = store.read_at(off, HEADER + len)?;
192 let Some(f) = unframe(&bytes) else { return Ok(false) };
193 if i == nth {
194 let mut payload = f.payload.to_vec();
195 edit(&mut payload);
196 if payload.len() != f.payload.len() {
197 return Ok(false);
198 }
199 store.write_at(off, &frame(f.seq, &f.prev, &payload))?;
200 store.sync()?;
201 return Ok(true);
202 }
203 off += f.size as u64;
204 }
205 Ok(false)
206}
207
208pub fn delete<S: Store>(store: &S, nth: usize) -> io::Result<bool> {
214 let mut off = 0u64;
215 let mut at = None;
216 let mut rest: Vec<(u64, Hash, Vec<u8>)> = vec![];
217 for i in 0.. {
218 let header = store.read_at(off, HEADER)?;
219 if header.len() < HEADER {
220 break;
221 }
222 let len = u32::from_le_bytes(header[0..4].try_into().expect("4 bytes")) as usize;
223 let bytes = store.read_at(off, HEADER + len)?;
224 let Some(f) = unframe(&bytes) else { break };
225 if i == nth {
226 at = Some(off);
227 } else if at.is_some() {
228 rest.push((f.seq - 1, f.prev, f.payload.to_vec()));
229 }
230 off += f.size as u64;
231 }
232 let Some(start) = at else { return Ok(false) };
233 let mut out = vec![];
234 for (seq, prev, payload) in &rest {
235 out.extend_from_slice(&frame(*seq, prev, payload));
236 }
237 let end = start + out.len() as u64;
238 out.resize((off - start) as usize, 0);
239 store.write_at(start, &out)?;
240 debug_assert!(end <= off);
241 store.sync()?;
242 Ok(true)
243}
244
245fn legacy_frame(bytes: &[u8]) -> bool {
247 if bytes.len() < HEADER {
248 return false;
249 }
250 let len = u32::from_le_bytes(bytes[0..4].try_into().expect("4 bytes")) as usize;
251 let sum = u32::from_le_bytes(bytes[4..8].try_into().expect("4 bytes"));
252 len > 0 && bytes.len() >= HEADER + len && crc32c::crc32c(&bytes[8..HEADER + len]) == sum
253}
254
255impl<S: Store> Journal<S> {
256 pub fn open(store: S) -> Result<Journal<S>, JournalError> {
258 let mut j = Journal {
259 store,
260 ledger: Ledger::default(),
261 offset: 0,
262 checkpoint_seq: 0,
263 head: Hash::ZERO,
264 pending_sentinel: None,
265 };
266 {
267 let _g = j.store.lock()?;
268 j.reload()?;
269 let next = j.ledger.next_txn.max(1);
272 j.store.forget_fences(|t| t.0 < next)?;
273 }
274 Ok(j)
275 }
276
277 fn load_checkpoint(&self) -> Result<Option<(u64, Ledger, Hash)>, JournalError> {
278 let Some(bytes) = self.store.read_checkpoint()? else {
279 let first = self.store.read_at(0, HEADER)?;
280 let len = if first.len() == HEADER { u32::from_le_bytes(first[0..4].try_into().expect("4")) } else { 0 };
281 if len > 0 && legacy_frame(&self.store.read_at(0, HEADER + len as usize)?) {
282 return Err(JournalError::Legacy);
283 }
284 return Ok(None);
285 };
286 let Some(f) = unframe(&bytes) else {
287 return Err(if legacy_frame(&bytes) {
288 JournalError::Legacy
289 } else {
290 JournalError::Integrity("the checkpoint fails its CRC".into())
291 });
292 };
293 let (seq, payload) = (f.seq, f.payload);
294 let ledger: Ledger = serde_json::from_slice(payload)
295 .map_err(|e| JournalError::Integrity(format!("the checkpoint does not parse: {e}")))?;
296 if ledger.last_seq != seq {
297 return Err(JournalError::Integrity("the checkpoint's seq disagrees with its ledger".into()));
298 }
299 if !ledger.well_formed() {
302 return Err(JournalError::Integrity("the checkpoint breaks the ledger invariant".into()));
303 }
304 Ok(Some((seq, ledger, f.prev)))
305 }
306
307 fn checkpoint_seq_on_disk(&self) -> Result<u64, JournalError> {
308 Ok(match self.store.read_checkpoint()? {
309 Some(b) => unframe(&b).map(|f| f.seq).unwrap_or(0),
310 None => 0,
311 })
312 }
313
314 fn reload(&mut self) -> Result<(), JournalError> {
315 match self.load_checkpoint()? {
316 Some((seq, ledger, head)) => {
317 self.ledger = ledger;
318 self.checkpoint_seq = seq;
319 self.head = head;
320 }
321 None => {
322 self.ledger = Ledger::default();
323 self.checkpoint_seq = 0;
324 self.head = Hash::ZERO;
325 }
326 }
327 self.offset = 0;
328 self.pending_sentinel = self.store.read_sentinel()?.and_then(|b| sentinel_txn(&b).map(|t| (t, b)));
331 let scanned = self.scan(true);
332 let pending = self.pending_sentinel.take();
333 scanned?;
334 if let Some((_, bytes)) = pending {
336 self.apply_sentinel(&bytes);
337 }
338 Ok(())
339 }
340
341 fn clear_resolved_sentinel(&mut self) -> Result<(), JournalError> {
346 if let Some(t) = self.store.read_sentinel()?.as_deref().and_then(sentinel_txn)
347 && !self.ledger.interlock.contains_key(&t)
348 {
349 self.store.write_sentinel(&[0; crate::store::SENTINEL_SIZE])?;
350 }
351 Ok(())
352 }
353
354 fn scan(&mut self, reload: bool) -> Result<(), JournalError> {
365 let truncate = reload;
366 let mut skipping = reload && self.offset == 0;
367 let mut prev: Option<u64> = None;
368 loop {
369 let header = self.store.read_at(self.offset, HEADER)?;
370 let len = if header.len() == HEADER {
371 u32::from_le_bytes(header[0..4].try_into().expect("4 bytes")) as usize
372 } else {
373 0
374 };
375 let next = header.len() == HEADER && {
383 let seq = u64::from_le_bytes(header[8..16].try_into().expect("8 bytes"));
384 let fits = len >= PREV && self.offset.saturating_add((HEADER + len) as u64) <= self.store.capacity();
385 fits && (seq == self.ledger.last_seq.wrapping_add(1) || (skipping && seq <= self.ledger.last_seq))
386 };
387 #[cfg(test)]
389 let next = next || tests::OLD_SCAN.with(std::cell::Cell::get);
390 let bytes = if len == 0 || !next { header } else { self.store.read_at(self.offset, HEADER + len)? };
391 let Some(f) = unframe(&bytes) else {
392 if truncate && bytes.iter().take(HEADER).any(|b| *b != 0) {
393 return self.stop_at_tail(truncate);
394 }
395 let after = self.store.read_at(self.offset + HEADER as u64, PREV)?;
400 if truncate
401 && after.iter().any(|b| *b != 0)
402 && let Some(seq) = self.later_record()?
403 {
404 return Err(JournalError::Integrity(format!(
405 "the record at byte {} was erased, but record {seq} follows it",
406 self.offset
407 )));
408 }
409 return Ok(());
410 };
411 let (seq, payload, size) = (f.seq, f.payload, f.size);
412 let consecutive = prev.is_none_or(|p| seq == p + 1);
413 if !consecutive {
414 return self.stop_at_tail(truncate);
415 }
416 if skipping && seq <= self.ledger.last_seq {
417 prev = Some(seq);
418 self.offset += size as u64;
419 continue;
420 }
421 skipping = false;
422 match chain::follows(self.ledger.last_seq, &self.head, seq, &f.prev) {
423 Follows::Yes => {}
424 Follows::OutOfSequence => return self.stop_at_tail(truncate),
425 Follows::Unchained => {
426 return Err(JournalError::Integrity(format!(
427 "record {seq} does not follow the hash chain (it may have been edited)"
428 )));
429 }
430 }
431 let entry: Entry = match serde_json::from_slice(payload) {
437 Ok(e) => e,
438 Err(e) => {
439 return Err(JournalError::Integrity(format!("record {seq} is intact but does not parse: {e}")));
440 }
441 };
442 if entry.seq != seq {
443 return self.stop_at_tail(truncate);
444 }
445 if let Record::Resolved { txn, .. } = &entry.record
446 && self.pending_sentinel.as_ref().is_some_and(|(t, _)| t == txn)
447 && let Some((_, bytes)) = self.pending_sentinel.take()
448 {
449 self.apply_sentinel(&bytes);
450 }
451 self.ledger
454 .apply(entry)
455 .map_err(|e| JournalError::Integrity(format!("record {seq} is refused on replay: {e:?}")))?;
456 self.head = chain(&f.prev, seq, payload);
457 prev = Some(seq);
458 self.offset += size as u64;
459 }
460 }
461
462 fn stop_at_tail(&mut self, truncate: bool) -> Result<(), JournalError> {
467 if truncate {
468 if let Some(seq) = self.later_record()? {
469 return Err(JournalError::Integrity(format!(
470 "the record at byte {} is damaged, but record {seq} follows it",
471 self.offset
472 )));
473 }
474 self.store.write_at(self.offset, &[0; HEADER])?;
475 self.store.sync()?;
476 }
477 Ok(())
478 }
479
480 fn later_record(&self) -> Result<Option<u64>, JournalError> {
484 let start = self.offset + 1;
485 let rest = self.store.read_at(start, self.store.capacity().saturating_sub(start) as usize)?;
486 let mut i = 0;
487 let (last, capacity) = (self.ledger.last_seq, self.store.capacity());
488 while i + HEADER <= rest.len() {
489 if could_follow(&rest[i..], last, capacity)
490 && let Some(f) = unframe(&rest[i..])
491 && f.seq > last
492 {
493 return Ok(Some(f.seq));
494 }
495 i += 1;
496 }
497 Ok(None)
498 }
499
500 pub fn verify_chain(&mut self) -> Result<(), JournalError> {
504 let _g = self.store.lock()?;
505 let (start, ckpt) = match self.load_checkpoint()? {
506 Some((seq, _, h)) => (h, seq),
507 None => (Hash::ZERO, 0),
508 };
509 let mut off = 0u64;
510 let mut links = Vec::new();
511 while off < self.offset {
512 let header = self.store.read_at(off, HEADER)?;
513 let len = if header.len() == HEADER {
514 u32::from_le_bytes(header[0..4].try_into().expect("4 bytes")) as usize
515 } else {
516 0
517 };
518 let bytes = self.store.read_at(off, HEADER + len)?;
519 let Some(f) = unframe(&bytes) else {
520 return Err(JournalError::Integrity(format!("the record at byte {off} is damaged")));
521 };
522 if f.seq <= ckpt {
523 off += f.size as u64;
524 continue;
525 }
526 links.push(Link { seq: f.seq, prev: f.prev, next: chain(&f.prev, f.seq, f.payload) });
527 off += f.size as u64;
528 }
529 let head = match chain::verify(ckpt, start, &links) {
531 Verdict::Chained { head, .. } => head,
532 Verdict::Break { at, .. } => {
533 return Err(JournalError::Integrity(format!(
534 "record {} does not follow the hash chain",
535 links[at].seq
536 )));
537 }
538 };
539 if ckpt == self.checkpoint_seq && head != self.head {
540 return Err(JournalError::Integrity("the journal no longer matches what was read".into()));
541 }
542 Ok(())
543 }
544
545 pub fn head(&self) -> Hash {
547 self.head
548 }
549
550 fn refresh(&mut self) -> Result<(), JournalError> {
552 if self.checkpoint_seq_on_disk()? != self.checkpoint_seq {
553 return self.reload();
555 }
556 self.scan(false)
557 }
558
559 pub fn ledger(&self) -> &Ledger {
560 &self.ledger
561 }
562
563 pub fn store(&self) -> &S {
564 &self.store
565 }
566
567 pub fn sync_ledger(&mut self) -> Result<&Ledger, JournalError> {
569 let _g = self.store.lock()?;
570 self.refresh()?;
571 Ok(&self.ledger)
572 }
573
574 pub fn append(&mut self, at: Timestamp, record: Record) -> Result<Entry, JournalError> {
576 let _g = self.store.lock()?;
577 self.refresh()?;
578 self.append_locked(at, record)
579 }
580
581 pub fn locked<R>(&mut self, f: impl FnOnce(&mut Locked<'_, S>) -> R) -> Result<R, JournalError> {
585 let _g = self.store.lock()?;
586 self.refresh()?;
587 Ok(f(&mut Locked(self)))
588 }
589
590 fn append_locked(&mut self, at: Timestamp, record: Record) -> Result<Entry, JournalError> {
591 let (seq, prev) = chain::next_link(self.ledger.last_seq, &self.head);
594 let entry = Entry { seq, at, record };
595 self.ledger.validate(&entry)?;
596 let payload = serde_json::to_vec(&entry).map_err(|e| JournalError::Corrupt(e.to_string()))?;
597 let bytes = frame(seq, &prev, &payload);
598 if self.offset + bytes.len() as u64 > self.store.capacity() {
599 return Err(JournalError::Full);
600 }
601 self.store.write_at(self.offset, &bytes)?;
602 self.store.sync()?;
603 self.ledger.apply(entry.clone())?;
604 self.head = chain(&prev, seq, &payload);
605 self.offset += bytes.len() as u64;
606 Ok(entry)
607 }
608
609 pub fn segment(&mut self) -> Result<Vec<Entry>, JournalError> {
612 let _g = self.store.lock()?;
613 let mut out = vec![];
614 let mut off = 0u64;
615 loop {
616 let header = self.store.read_at(off, HEADER)?;
617 if header.len() < HEADER {
618 break;
619 }
620 let len = u32::from_le_bytes(header[0..4].try_into().expect("4 bytes")) as usize;
621 let bytes = self.store.read_at(off, HEADER + len)?;
622 let Some(f) = unframe(&bytes) else { break };
623 if f.seq <= self.checkpoint_seq || out.last().is_some_and(|e: &Entry| e.seq + 1 != f.seq) {
624 break;
625 }
626 match serde_json::from_slice::<Entry>(f.payload) {
627 Ok(e) => out.push(e),
628 Err(_) => break,
629 }
630 off += f.size as u64;
631 }
632 Ok(out)
633 }
634
635 pub fn offset(&self) -> u64 {
637 self.offset
638 }
639
640 pub fn usage_permille(&self) -> u64 {
642 self.offset * 1000 / self.store.capacity().max(1)
643 }
644
645 pub fn needs_compaction(&self) -> bool {
647 self.usage_permille() >= 750
648 }
649
650 pub fn compact(&mut self) -> Result<(), JournalError> {
652 let _g = self.store.lock()?;
653 self.refresh()?;
654 let payload = serde_json::to_vec(&self.ledger).map_err(|e| JournalError::Corrupt(e.to_string()))?;
655 self.store.write_checkpoint(&frame(self.ledger.last_seq, &self.head, &payload))?;
656 self.checkpoint_seq = self.ledger.last_seq;
657 self.store.write_at(0, &[0; HEADER])?;
658 self.store.sync()?;
659 self.offset = 0;
660 let live = &self.ledger.live;
663 self.store.forget_fences(|t| live.get(&t).is_some())?;
664 self.clear_resolved_sentinel()
665 }
666
667 pub fn write_sentinel(&mut self, txn: TxnId, note: &str) -> Result<(), JournalError> {
671 let text = format!("RecoveryFailed txn={} {}", txn.0, note);
672 self.store.write_sentinel(text.as_bytes())?;
673 self.apply_sentinel(text.as_bytes());
674 Ok(())
675 }
676
677 fn apply_sentinel(&mut self, bytes: &[u8]) {
678 let Some(txn) = sentinel_txn(bytes) else { return };
679 let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
680 let text = String::from_utf8_lossy(&bytes[..end]).into_owned();
681 if self.ledger.interlock.contains_key(&txn) {
682 return;
683 }
684 let (transition, revert_to) = match self.ledger.live.get(&txn) {
685 Some(l) => (l.txn.transition.clone(), l.txn.revert_to),
686 None => (Transition::Rollback, GenerationId(0)),
687 };
688 self.ledger.add_interlock(InterlockEntry {
689 txn,
690 transition,
691 failed_from: "sentinel".into(),
692 base_override: None,
693 candidate_override: None,
694 revert_to,
695 note: text,
696 });
697 }
698}
699
700fn sentinel_txn(bytes: &[u8]) -> Option<TxnId> {
702 let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
703 let text = std::str::from_utf8(&bytes[..end]).ok()?;
704 let id = text.strip_prefix("RecoveryFailed txn=")?.split_whitespace().next()?;
705 id.parse::<u64>().ok().map(TxnId)
706}
707
708#[cfg(test)]
709mod tests;