1#[path = "txlog_tee.rs"]
82pub mod tee;
83
84use std::fs::{self, File, OpenOptions};
85use std::io::{self, Read, Seek, SeekFrom, Write};
86use std::path::{Path, PathBuf};
87use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
88use std::sync::{Arc, Mutex, Weak};
89
90use serde::{Deserialize, Serialize};
91
92const MAGIC: [u8; 4] = *b"GTX1";
93const HEADER_LEN: usize = 32;
94const TRAILER_LEN: usize = 4;
95const SEG_PREFIX: &str = "seg-";
96const SEG_EXT: &str = "log";
97const IDX_EXT: &str = "idx";
98
99pub const DEFAULT_SEGMENT_BYTES: u64 = 1 << 30;
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
107#[repr(u16)]
108pub enum Kind {
109 Pack = 1,
112 Refs = 2,
116 ControlBlob = 3,
119 ControlDoc = 4,
125 Secret = 5,
128 LfsPut = 6,
130 LfsDelete = 7,
132 RepoStore = 8,
135 Kek = 9,
141 Append = 10,
145 File = 11,
148 SnapshotAt = 12,
153 Redacted = 13,
161 Erase = 14,
167 Activity = 15,
173}
174
175impl Kind {
176 pub fn from_u16(v: u16) -> Option<Self> {
179 Some(match v {
180 1 => Kind::Pack,
181 2 => Kind::Refs,
182 3 => Kind::ControlBlob,
183 4 => Kind::ControlDoc,
184 5 => Kind::Secret,
185 6 => Kind::LfsPut,
186 7 => Kind::LfsDelete,
187 8 => Kind::RepoStore,
188 9 => Kind::Kek,
189 10 => Kind::Append,
190 11 => Kind::File,
191 12 => Kind::SnapshotAt,
192 13 => Kind::Redacted,
193 14 => Kind::Erase,
194 15 => Kind::Activity,
195 _ => return None,
196 })
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct RefEdit {
205 pub name: String,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub old: Option<Option<String>>,
212 pub new: Option<String>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub peeled: Option<String>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub symref: Option<String>,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(tag = "k", rename_all = "snake_case")]
225pub enum Meta {
226 Pack {
228 repo: String,
230 hash: String,
232 },
233 Refs {
235 repo: String,
237 atomic: bool,
239 edits: Vec<RefEdit>,
241 },
242 ControlBlob {
244 id: String,
246 },
247 ControlDoc {
249 name: String,
251 expected: Option<String>,
253 new: String,
255 },
256 Secret {
258 key: String,
260 deleted: bool,
262 },
263 Lfs {
265 repo: String,
267 oid: String,
269 size: u64,
271 },
272 RepoStore {
274 repo: String,
276 op: String,
278 hash: String,
280 },
281 Kek {},
283 Append {
285 log: String,
287 },
288 File {
290 path: String,
292 deleted: bool,
294 },
295 Activity {},
297 SnapshotAt {
299 at: u64,
301 },
302 Erase {
304 account: String,
306 repos: Vec<String>,
309 reason: String,
311 },
312}
313
314impl Meta {
315 pub fn repo(&self) -> Option<&str> {
317 match self {
318 Meta::Pack { repo, .. } | Meta::Refs { repo, .. } | Meta::Lfs { repo, .. } | Meta::RepoStore { repo, .. } => Some(repo),
319 _ => None,
320 }
321 }
322}
323
324#[derive(Debug, Clone, Default, PartialEq, Eq)]
326pub struct Erased {
327 pub frames: u64,
329 pub bytes: u64,
331}
332
333impl Meta {
334 pub fn to_bytes(&self) -> Vec<u8> {
336 serde_json::to_vec(self).expect("Meta serializes")
337 }
338
339 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
341 serde_json::from_slice(bytes).map_err(|e| Error::BadMeta(e.to_string()))
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct Entry {
348 pub seq: u64,
350 pub kind: u16,
352 pub meta: Vec<u8>,
354 pub blob: Vec<u8>,
356}
357
358impl Entry {
359 pub fn kind(&self) -> Option<Kind> {
361 Kind::from_u16(self.kind)
362 }
363 pub fn meta(&self) -> Result<Meta> {
365 Meta::from_bytes(&self.meta)
366 }
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct Located {
373 pub seq: u64,
375 pub kind: u16,
377 pub meta: Vec<u8>,
379 pub path: PathBuf,
381 pub blob_offset: u64,
383 pub blob_len: u64,
385 pub payload_crc: Option<u32>,
394}
395
396#[derive(Debug, thiserror::Error)]
398pub enum Error {
399 #[error("txlog: {0}")]
401 Io(#[from] io::Error),
402 #[error("txlog: bad meta: {0}")]
404 BadMeta(String),
405 #[error("txlog: seq {seq} not retained (first retained is {first})")]
407 NotRetained {
408 seq: u64,
410 first: u64,
412 },
413 #[error("txlog: seq {seq} is beyond head {head}")]
415 BeyondHead {
416 seq: u64,
418 head: u64,
420 },
421 #[error("txlog: corrupt frame at {path}:{offset}: {what}")]
423 Corrupt {
424 path: PathBuf,
426 offset: u64,
428 what: &'static str,
430 },
431 #[error("txlog: released — the files under {0} are given up while the data set is grown; writes resume after the remount")]
434 Released(PathBuf),
435 #[error("txlog: append out of order: got {got}, expected {expected}")]
437 OutOfOrder {
438 got: u64,
440 expected: u64,
442 },
443 #[error("txlog: this box is a twin (or restoring); it originates no transactions")]
447 Follower,
448 #[error("txlog: unexpected file {0}")]
450 Unexpected(PathBuf),
451 #[error("txlog: poisoned under {dir} — {cause}; every write refuses until the log is reopened from its last durable frame")]
459 Poisoned {
460 dir: PathBuf,
462 cause: String,
464 },
465}
466
467pub type Result<T> = std::result::Result<T, Error>;
469
470const CRC32C_POLY: u32 = 0x82F6_3B78;
473
474const fn crc32c_table() -> [u32; 256] {
475 let mut table = [0u32; 256];
476 let mut i = 0;
477 while i < 256 {
478 let mut c = i as u32;
479 let mut k = 0;
480 while k < 8 {
481 c = if c & 1 != 0 { CRC32C_POLY ^ (c >> 1) } else { c >> 1 };
482 k += 1;
483 }
484 table[i] = c;
485 i += 1;
486 }
487 table
488}
489
490static CRC32C_TABLE: [u32; 256] = crc32c_table();
491
492#[derive(Debug, Clone, Copy)]
494pub struct Crc32c(u32);
495
496impl Default for Crc32c {
497 fn default() -> Self {
498 Self::new()
499 }
500}
501
502impl Crc32c {
503 pub const fn new() -> Self {
505 Crc32c(0xFFFF_FFFF)
506 }
507 pub fn update(&mut self, bytes: &[u8]) {
509 let mut c = self.0;
510 for &b in bytes {
511 c = CRC32C_TABLE[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
512 }
513 self.0 = c;
514 }
515 pub fn finish(self) -> u32 {
517 self.0 ^ 0xFFFF_FFFF
518 }
519 pub fn state(&self) -> u32 {
523 self.0
524 }
525 pub const fn resume(state: u32) -> Self {
527 Crc32c(state)
528 }
529}
530
531pub fn crc32c(bytes: &[u8]) -> u32 {
533 let mut c = Crc32c::new();
534 c.update(bytes);
535 c.finish()
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
541struct Header {
542 seq: u64,
543 kind: u16,
544 meta_len: u32,
545 blob_len: u64,
546}
547
548impl Header {
549 fn encode(&self) -> [u8; HEADER_LEN] {
550 let mut h = [0u8; HEADER_LEN];
551 h[0..4].copy_from_slice(&MAGIC);
552 h[4..12].copy_from_slice(&self.seq.to_le_bytes());
553 h[12..14].copy_from_slice(&self.kind.to_le_bytes());
554 h[14..16].copy_from_slice(&0u16.to_le_bytes());
555 h[16..20].copy_from_slice(&self.meta_len.to_le_bytes());
556 h[20..28].copy_from_slice(&self.blob_len.to_le_bytes());
557 let crc = crc32c(&h[0..28]);
558 h[28..32].copy_from_slice(&crc.to_le_bytes());
559 h
560 }
561
562 fn decode(h: &[u8]) -> Option<Self> {
563 if h.len() < HEADER_LEN || h[0..4] != MAGIC {
564 return None;
565 }
566 let crc = u32::from_le_bytes(h[28..32].try_into().ok()?);
567 if crc32c(&h[0..28]) != crc {
568 return None;
569 }
570 Some(Header {
571 seq: u64::from_le_bytes(h[4..12].try_into().ok()?),
572 kind: u16::from_le_bytes(h[12..14].try_into().ok()?),
573 meta_len: u32::from_le_bytes(h[16..20].try_into().ok()?),
574 blob_len: u64::from_le_bytes(h[20..28].try_into().ok()?),
575 })
576 }
577
578 fn frame_len(&self) -> u64 {
579 HEADER_LEN as u64 + self.meta_len as u64 + self.blob_len + TRAILER_LEN as u64
580 }
581}
582
583fn seg_path(dir: &Path, first: u64) -> PathBuf {
586 dir.join(format!("{SEG_PREFIX}{first:020}.{SEG_EXT}"))
587}
588
589fn idx_path(dir: &Path, first: u64) -> PathBuf {
590 dir.join(format!("{SEG_PREFIX}{first:020}.{IDX_EXT}"))
591}
592
593fn parse_seg_name(name: &str) -> Option<(u64, bool)> {
594 let rest = name.strip_prefix(SEG_PREFIX)?;
595 let (num, ext) = rest.split_once('.')?;
596 let first: u64 = num.parse().ok()?;
597 match ext {
598 SEG_EXT => Some((first, true)),
599 IDX_EXT => Some((first, false)),
600 _ => None,
601 }
602}
603
604fn sync_dir(dir: &Path) -> io::Result<()> {
605 File::open(dir)?.sync_all()
606}
607
608fn zero_range(file: &mut File, at: u64, len: u64) -> io::Result<()> {
611 if len == 0 {
612 return Ok(());
613 }
614 #[cfg(target_os = "linux")]
615 {
616 use rustix::fs::{fallocate, FallocateFlags};
617 if fallocate(&*file, FallocateFlags::PUNCH_HOLE | FallocateFlags::KEEP_SIZE, at, len).is_ok() {
618 return Ok(());
619 }
620 }
621 let zeros = vec![0u8; (1usize << 20).min(len as usize)];
622 file.seek(SeekFrom::Start(at))?;
623 let mut left = len;
624 while left > 0 {
625 let n = left.min(zeros.len() as u64) as usize;
626 file.write_all(&zeros[..n])?;
627 left -= n as u64;
628 }
629 Ok(())
630}
631
632#[derive(Debug)]
634struct Segment {
635 first: u64,
636 offsets: Vec<u64>,
638 len: u64,
640}
641
642impl Segment {
643 fn last_seq(&self) -> Option<u64> {
644 if self.offsets.is_empty() {
645 None
646 } else {
647 Some(self.first + self.offsets.len() as u64 - 1)
648 }
649 }
650}
651
652fn scan_frames(
655 path: &Path,
656 file: &mut File,
657 first: u64,
658 offsets: &mut Vec<u64>,
659 from: u64,
660) -> Result<(u64, bool)> {
661 let total = file.metadata()?.len();
662 let mut pos = from;
663 let mut hdr = [0u8; HEADER_LEN];
664 let mut buf = Vec::new();
665 while pos < total {
666 if total - pos < HEADER_LEN as u64 {
667 return Ok((pos, true));
668 }
669 file.seek(SeekFrom::Start(pos))?;
670 file.read_exact(&mut hdr)?;
671 let Some(h) = Header::decode(&hdr) else {
672 return Ok((pos, true));
673 };
674 let expect = first + offsets.len() as u64;
675 if h.seq != expect {
676 return Ok((pos, true));
677 }
678 let payload = h.meta_len as u64 + h.blob_len;
679 if total - pos < h.frame_len() {
680 return Ok((pos, true));
681 }
682 if h.kind == Kind::Redacted as u16 {
685 offsets.push(pos);
686 pos += h.frame_len();
687 continue;
688 }
689 let mut crc = Crc32c::new();
691 let mut remaining = payload;
692 buf.resize(1 << 20, 0);
693 while remaining > 0 {
694 let n = remaining.min(buf.len() as u64) as usize;
695 file.read_exact(&mut buf[..n])?;
696 crc.update(&buf[..n]);
697 remaining -= n as u64;
698 }
699 let mut tr = [0u8; TRAILER_LEN];
700 file.read_exact(&mut tr)?;
701 if u32::from_le_bytes(tr) != crc.finish() {
702 return Ok((pos, true));
703 }
704 let _ = path;
705 offsets.push(pos);
706 pos += h.frame_len();
707 }
708 Ok((pos, false))
709}
710
711fn open_segment(dir: &Path, first: u64, repair: bool) -> Result<Segment> {
712 let path = seg_path(dir, first);
713 let mut file = OpenOptions::new().read(true).write(repair).open(&path)?;
714 let total = file.metadata()?.len();
715
716 let mut offsets: Vec<u64> = Vec::new();
718 let ipath = idx_path(dir, first);
719 if let Ok(bytes) = fs::read(&ipath) {
720 let n = bytes.len() / 8;
721 offsets.reserve(n);
722 for i in 0..n {
723 let off = u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().expect("8 bytes"));
724 offsets.push(off);
725 }
726 let monotone = offsets.windows(2).all(|w| w[0] < w[1]);
728 if !monotone || offsets.first().is_some_and(|&o| o != 0) {
729 offsets.clear();
730 }
731 while let Some(&last) = offsets.last() {
732 if last + HEADER_LEN as u64 > total {
733 offsets.pop();
734 continue;
735 }
736 file.seek(SeekFrom::Start(last))?;
737 let mut hdr = [0u8; HEADER_LEN];
738 file.read_exact(&mut hdr)?;
739 match Header::decode(&hdr) {
740 Some(h) if h.seq == first + offsets.len() as u64 - 1 && last + h.frame_len() <= total => {
741 break;
742 }
743 _ => {
744 offsets.pop();
745 }
746 }
747 }
748 }
749
750 let trusted = offsets.len();
756 let resume = offsets.pop().unwrap_or(0);
757 let (valid_len, torn) = scan_frames(&path, &mut file, first, &mut offsets, resume)?;
758
759 if torn && repair {
760 tracing::warn!(
761 segment = %path.display(),
762 kept = valid_len,
763 discarded = total - valid_len,
764 "txlog: torn tail cut"
765 );
766 file.set_len(valid_len)?;
767 file.sync_all()?;
768 }
769 if (torn && repair) || offsets.len() != trusted {
770 if repair {
772 let mut bytes = Vec::with_capacity(offsets.len() * 8);
773 for off in &offsets {
774 bytes.extend_from_slice(&off.to_le_bytes());
775 }
776 fs::write(&ipath, bytes)?;
777 }
778 }
779 Ok(Segment {
780 first,
781 offsets,
782 len: valid_len,
783 })
784}
785
786struct Writer {
789 file: File,
791 idx: File,
793 seg: Segment,
797 disk_len: u64,
799 buf: Vec<u8>,
804 published: usize,
807 #[cfg(test)]
809 faults: WriterFaults,
810}
811
812#[cfg(test)]
814#[derive(Default)]
815struct WriterFaults {
816 fail_put_after: Option<usize>,
818 fail_cut: bool,
820}
821
822const STAGE_BYTES: usize = 1 << 20;
824const DIRECT_BYTES: usize = 64 << 10;
827
828impl Writer {
829 fn flush_buf(&mut self) -> io::Result<()> {
831 if !self.buf.is_empty() {
832 self.file.write_all(&self.buf)?;
833 self.disk_len += self.buf.len() as u64;
834 self.buf.clear();
835 }
836 Ok(())
837 }
838
839 fn put(&mut self, bytes: &[u8]) -> io::Result<()> {
841 #[cfg(test)]
842 if let Some(n) = self.faults.fail_put_after {
843 if n == 0 {
844 self.faults.fail_put_after = None;
845 return Err(io::Error::other("txlog test: injected write failure"));
846 }
847 self.faults.fail_put_after = Some(n - 1);
848 }
849 if bytes.len() >= DIRECT_BYTES {
850 self.flush_buf()?;
851 self.file.write_all(bytes)?;
852 self.disk_len += bytes.len() as u64;
853 return Ok(());
854 }
855 self.buf.extend_from_slice(bytes);
856 if self.buf.len() >= STAGE_BYTES {
857 self.flush_buf()?;
858 }
859 Ok(())
860 }
861
862 fn unpublished(&self) -> usize {
864 self.seg.offsets.len() - self.published
865 }
866
867 fn cut_to_committed(&mut self) -> io::Result<()> {
869 #[cfg(test)]
870 if std::mem::take(&mut self.faults.fail_cut) {
871 return Err(io::Error::other("txlog test: injected truncate failure"));
872 }
873 let keep = self.seg.len;
874 if keep >= self.disk_len {
875 self.buf.truncate((keep - self.disk_len) as usize);
877 return Ok(());
878 }
879 self.buf.clear();
882 self.file.set_len(keep)?;
883 self.file.seek(SeekFrom::Start(keep))?;
884 self.disk_len = keep;
885 self.file.sync_data()
886 }
887}
888
889pub const TEST_GROUP_ENV: &str = "GUNNAR_TXLOG_TEST_GROUP";
895
896#[cfg(feature = "test-hooks")]
899fn test_group_from_env() -> Option<usize> {
900 std::env::var(TEST_GROUP_ENV).ok().and_then(|v| v.trim().parse::<usize>().ok()).filter(|n| *n > 1)
901}
902
903#[cfg(not(feature = "test-hooks"))]
904fn test_group_from_env() -> Option<usize> {
905 None
906}
907
908pub const TEST_GROUP_WAIT: std::time::Duration = std::time::Duration::from_millis(100);
911
912pub struct Log {
917 dir: PathBuf,
918 segments: Mutex<Vec<Segment>>,
920 writer: Mutex<Writer>,
921 head: AtomicU64,
922 first: AtomicU64,
923 segment_bytes: u64,
924 follower: AtomicBool,
926 gate: Arc<Gate>,
928 released: AtomicBool,
930 poisoned: Mutex<Option<String>>,
932 sync_state: Mutex<bool>,
940 synced: std::sync::Condvar,
942 #[cfg(test)]
944 fsync_gates: Mutex<std::collections::VecDeque<std::sync::mpsc::Receiver<()>>>,
945 #[cfg(test)]
947 fail_fsyncs: std::sync::atomic::AtomicU32,
948 test_group: Option<usize>,
952 frame_syncs: AtomicU64,
954 erase_syncs: AtomicU64,
956 erase_lock: Mutex<()>,
958 #[cfg(test)]
960 erase_gates: Mutex<std::collections::VecDeque<std::sync::mpsc::Receiver<()>>>,
961}
962
963#[derive(Default)]
969struct Gate {
970 state: Mutex<GateState>,
971 cv: std::sync::Condvar,
972}
973
974#[derive(Default)]
975struct GateState {
976 frozen_by: Option<std::thread::ThreadId>,
977 inflight: u64,
978}
979
980thread_local! {
981 static MUTATION_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
983}
984
985#[must_use = "a mutation guard must be held until the store has taken the write"]
987pub struct MutationGuard {
988 gate: Option<Arc<Gate>>,
989}
990
991impl Drop for MutationGuard {
992 fn drop(&mut self) {
993 MUTATION_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
994 if let Some(gate) = self.gate.take() {
995 let mut st = gate.state.lock().unwrap_or_else(|p| p.into_inner());
996 st.inflight = st.inflight.saturating_sub(1);
997 gate.cv.notify_all();
998 }
999 }
1000}
1001
1002pub struct FreezeGuard {
1004 gate: Arc<Gate>,
1005}
1006
1007impl Drop for FreezeGuard {
1008 fn drop(&mut self) {
1009 let mut st = self.gate.state.lock().unwrap_or_else(|p| p.into_inner());
1010 st.frozen_by = None;
1011 self.gate.cv.notify_all();
1012 }
1013}
1014
1015impl std::fmt::Debug for Log {
1016 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1017 f.debug_struct("Log")
1018 .field("dir", &self.dir)
1019 .field("first", &self.first_seq())
1020 .field("head", &self.head_seq())
1021 .finish()
1022 }
1023}
1024
1025pub const SPOOL_DIR: &str = "spool";
1040
1041impl Log {
1042 pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
1044 Self::open_with(dir, DEFAULT_SEGMENT_BYTES)
1045 }
1046
1047 pub fn open_with(dir: impl AsRef<Path>, segment_bytes: u64) -> Result<Self> {
1049 let dir = dir.as_ref().to_path_buf();
1050 fs::create_dir_all(&dir)?;
1051 let mut firsts: Vec<u64> = Vec::new();
1052 for entry in fs::read_dir(&dir)? {
1053 let entry = entry?;
1054 let name = entry.file_name();
1055 let name = name.to_string_lossy();
1056 match parse_seg_name(&name) {
1057 Some((first, true)) => firsts.push(first),
1058 Some((_, false)) => {}
1059 None if name == SPOOL_DIR && entry.file_type().is_ok_and(|t| t.is_dir()) => {}
1065 None => return Err(Error::Unexpected(entry.path())),
1066 }
1067 }
1068 firsts.sort_unstable();
1069 let mut segments = Vec::with_capacity(firsts.len().max(1));
1070 let n = firsts.len();
1071 for (i, first) in firsts.iter().enumerate() {
1072 let seg = open_segment(&dir, *first, true)?;
1073 if i + 1 < n && seg.offsets.is_empty() {
1076 return Err(Error::Corrupt {
1077 path: seg_path(&dir, *first),
1078 offset: 0,
1079 what: "closed segment holds no frame",
1080 });
1081 }
1082 if let Some(next) = firsts.get(i + 1) {
1084 match seg.last_seq() {
1085 Some(last) if last + 1 == *next => {}
1086 _ => {
1087 return Err(Error::Corrupt {
1088 path: seg_path(&dir, *first),
1089 offset: seg.len,
1090 what: "segment does not run up to the next one",
1091 })
1092 }
1093 }
1094 }
1095 segments.push(seg);
1096 }
1097 if segments.is_empty() {
1098 let first = 1;
1099 File::create(seg_path(&dir, first))?;
1100 File::create(idx_path(&dir, first))?;
1101 sync_dir(&dir)?;
1102 segments.push(Segment {
1103 first,
1104 offsets: Vec::new(),
1105 len: 0,
1106 });
1107 }
1108 let open = segments.last().expect("one segment");
1109 let head = open.last_seq().unwrap_or(open.first - 1);
1110 let first = segments[0].first;
1111 let mut file = OpenOptions::new().append(true).open(seg_path(&dir, open.first))?;
1112 file.seek(SeekFrom::Start(open.len))?;
1113 let idx = OpenOptions::new().append(true).open(idx_path(&dir, open.first))?;
1114 let seg = Segment {
1115 first: open.first,
1116 offsets: open.offsets.clone(),
1117 len: open.len,
1118 };
1119 let published = seg.offsets.len();
1120 Ok(Log {
1121 dir,
1122 segments: Mutex::new(segments),
1123 writer: Mutex::new(Writer {
1124 file,
1125 idx,
1126 disk_len: seg.len,
1127 seg,
1128 buf: Vec::new(),
1129 published,
1130 #[cfg(test)]
1131 faults: WriterFaults::default(),
1132 }),
1133 head: AtomicU64::new(head),
1134 first: AtomicU64::new(first),
1135 segment_bytes,
1136 follower: AtomicBool::new(false),
1137 gate: Arc::new(Gate::default()),
1138 released: AtomicBool::new(false),
1139 poisoned: Mutex::new(None),
1140 sync_state: Mutex::new(false),
1141 synced: std::sync::Condvar::new(),
1142 #[cfg(test)]
1143 fsync_gates: Mutex::new(Default::default()),
1144 #[cfg(test)]
1145 fail_fsyncs: std::sync::atomic::AtomicU32::new(0),
1146 test_group: test_group_from_env(),
1147 frame_syncs: AtomicU64::new(0),
1148 erase_syncs: AtomicU64::new(0),
1149 erase_lock: Mutex::new(()),
1150 #[cfg(test)]
1151 erase_gates: Mutex::new(Default::default()),
1152 })
1153 }
1154
1155 pub fn mutation(&self) -> MutationGuard {
1160 let depth = MUTATION_DEPTH.with(|d| {
1161 let v = d.get();
1162 d.set(v + 1);
1163 v
1164 });
1165 if depth > 0 {
1166 return MutationGuard { gate: None };
1167 }
1168 let me = std::thread::current().id();
1169 let mut st = self.gate.state.lock().unwrap_or_else(|p| p.into_inner());
1170 while matches!(st.frozen_by, Some(t) if t != me) {
1171 st = self.gate.cv.wait(st).unwrap_or_else(|p| p.into_inner());
1172 }
1173 st.inflight += 1;
1174 MutationGuard { gate: Some(Arc::clone(&self.gate)) }
1175 }
1176
1177 pub fn freeze(&self) -> FreezeGuard {
1181 debug_assert_eq!(MUTATION_DEPTH.with(|d| d.get()), 0, "freeze from inside a mutation waits for itself");
1182 let me = std::thread::current().id();
1183 let mut st = self.gate.state.lock().unwrap_or_else(|p| p.into_inner());
1184 while st.frozen_by.is_some_and(|t| t != me) {
1185 st = self.gate.cv.wait(st).unwrap_or_else(|p| p.into_inner());
1186 }
1187 st.frozen_by = Some(me);
1188 while st.inflight > 0 {
1189 st = self.gate.cv.wait(st).unwrap_or_else(|p| p.into_inner());
1190 }
1191 FreezeGuard { gate: Arc::clone(&self.gate) }
1192 }
1193
1194 pub fn dir(&self) -> &Path {
1196 &self.dir
1197 }
1198
1199 pub fn set_follower(&self, on: bool) {
1202 self.follower.store(on, Ordering::Release);
1203 }
1204
1205 pub fn is_follower(&self) -> bool {
1207 self.follower.load(Ordering::Acquire)
1208 }
1209
1210 pub fn group_hold(&self) -> Option<usize> {
1213 self.test_group
1214 }
1215
1216 pub fn head_seq(&self) -> u64 {
1218 self.head.load(Ordering::Acquire)
1219 }
1220
1221 pub fn first_seq(&self) -> u64 {
1223 self.first.load(Ordering::Acquire)
1224 }
1225
1226 pub fn append(&self, kind: Kind, meta: &Meta, blob: &[u8]) -> Result<u64> {
1229 if self.is_follower() {
1230 return Err(Error::Follower);
1231 }
1232 let meta = meta.to_bytes();
1233 self.append_raw(kind as u16, &meta, blob)
1234 }
1235
1236 pub fn append_at(&self, seq: u64, kind: u16, meta: &[u8], blob: &[u8]) -> Result<u64> {
1240 {
1241 let mut w = self.lock_writer()?;
1242 let expected = w.seg.first + w.seg.offsets.len() as u64;
1243 if seq != expected {
1244 return Err(Error::OutOfOrder { got: seq, expected });
1245 }
1246 self.stage_locked(&mut w, kind, meta, blob)?;
1247 }
1248 self.sync_through(seq)?;
1249 Ok(seq)
1250 }
1251
1252 pub fn append_raw(&self, kind: u16, meta: &[u8], blob: &[u8]) -> Result<u64> {
1260 let seq = {
1261 let mut w = self.lock_writer()?;
1262 self.stage_locked(&mut w, kind, meta, blob)?
1263 };
1264 self.sync_through(seq)?;
1265 Ok(seq)
1266 }
1267
1268 pub fn stage(&self, kind: Kind, meta: &Meta, blob: &[u8]) -> Result<u64> {
1273 if self.is_follower() {
1274 return Err(Error::Follower);
1275 }
1276 self.stage_raw(kind as u16, &meta.to_bytes(), blob)
1277 }
1278
1279 pub fn stage_raw(&self, kind: u16, meta: &[u8], blob: &[u8]) -> Result<u64> {
1284 let mut w = self.lock_writer()?;
1285 self.stage_locked(&mut w, kind, meta, blob)
1286 }
1287
1288 fn stage_locked(&self, w: &mut Writer, kind: u16, meta: &[u8], blob: &[u8]) -> Result<u64> {
1289 self.roll_if_full(w)?;
1290 let seq = w.seg.first + w.seg.offsets.len() as u64;
1291 let header = Header {
1292 seq,
1293 kind,
1294 meta_len: meta.len() as u32,
1295 blob_len: blob.len() as u64,
1296 };
1297 let h = header.encode();
1298 let mut crc = Crc32c::new();
1299 crc.update(meta);
1300 crc.update(blob);
1301 let tr = crc.finish().to_le_bytes();
1302 let put = (|| {
1303 w.put(&h)?;
1304 w.put(meta)?;
1305 w.put(blob)?;
1306 w.put(&tr)
1307 })();
1308 if let Err(e) = put {
1309 if let Err(cut) = w.cut_to_committed() {
1312 self.poison(format!("a half-written frame could not be cut off the segment ({cut}) after its write failed ({e})"));
1313 }
1314 return Err(e.into());
1315 }
1316 let off = w.seg.len;
1317 w.seg.offsets.push(off);
1318 w.seg.len += header.frame_len();
1319 Ok(seq)
1320 }
1321
1322 pub fn sync_through(&self, seq: u64) -> Result<()> {
1329 {
1330 let mut leading = self.sync_state.lock().unwrap_or_else(|p| p.into_inner());
1331 loop {
1332 if self.head_seq() >= seq {
1333 return Ok(());
1334 }
1335 if !*leading {
1336 *leading = true;
1337 break;
1338 }
1339 leading = self.synced.wait(leading).unwrap_or_else(|p| p.into_inner());
1340 }
1341 }
1342 struct Lead<'a>(&'a Log);
1345 impl Drop for Lead<'_> {
1346 fn drop(&mut self) {
1347 *self.0.sync_state.lock().unwrap_or_else(|p| p.into_inner()) = false;
1348 self.0.synced.notify_all();
1349 }
1350 }
1351 let _lead = Lead(self);
1352 if let Some(n) = self.test_group {
1353 self.hold_for_the_group(n);
1354 }
1355 let snap = (|| -> Result<(File, u64, usize)> {
1356 let mut w = self.lock_writer()?;
1357 w.flush_buf()?;
1358 Ok((w.file.try_clone()?, w.seg.first, w.seg.offsets.len()))
1359 })();
1360 let (file, first, upto) = match snap {
1361 Ok(s) => s,
1362 Err(_) if self.head_seq() >= seq => return Ok(()),
1364 Err(e) => return Err(e),
1365 };
1366 #[cfg(test)]
1367 {
1368 let gate = self.fsync_gates.lock().unwrap_or_else(|p| p.into_inner()).pop_front();
1369 if let Some(gate) = gate {
1370 let _ = gate.recv();
1371 }
1372 }
1373 if let Err(e) = self.sync_frames(&file) {
1374 let mut w = self.writer.lock().expect("txlog writer");
1375 if w.seg.first == first {
1376 return Err(self.poison_and_cut(&mut w, e));
1377 }
1378 drop(w);
1381 if self.head_seq() < seq {
1382 return Err(self.poison_and_cut(&mut self.writer.lock().expect("txlog writer"), e));
1383 }
1384 return Ok(());
1385 }
1386 self.frame_syncs.fetch_add(1, Ordering::Relaxed);
1387 let mut w = match self.lock_writer() {
1388 Ok(w) => w,
1389 Err(_) if self.head_seq() >= seq => return Ok(()),
1390 Err(e) => return Err(e),
1391 };
1392 if w.seg.first == first && upto > w.published {
1394 self.publish_upto(&mut w, upto)?;
1395 }
1396 Ok(())
1397 }
1398
1399 fn hold_for_the_group(&self, n: usize) {
1404 let until = std::time::Instant::now() + TEST_GROUP_WAIT;
1405 loop {
1406 let staged = self.writer.lock().map(|w| w.unpublished()).unwrap_or(n);
1407 if staged >= n || std::time::Instant::now() >= until {
1408 return;
1409 }
1410 std::thread::sleep(std::time::Duration::from_micros(200));
1411 }
1412 }
1413
1414 fn sync_frames(&self, file: &File) -> io::Result<()> {
1417 #[cfg(test)]
1418 loop {
1419 let n = self.fail_fsyncs.load(Ordering::SeqCst);
1420 if n == 0 {
1421 break;
1422 }
1423 if self.fail_fsyncs.compare_exchange(n, n - 1, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
1424 return Err(io::Error::other("txlog test: injected fsync failure (EIO)"));
1425 }
1426 }
1427 file.sync_data()
1428 }
1429
1430 #[cfg(test)]
1432 fn fail_next_fsyncs(&self, n: u32) {
1433 self.fail_fsyncs.store(n, Ordering::SeqCst);
1434 }
1435
1436 #[cfg(test)]
1438 fn hold_next_fsync(&self, gate: std::sync::mpsc::Receiver<()>) {
1439 self.fsync_gates.lock().unwrap().push_back(gate);
1440 }
1441
1442 pub fn sync(&self) -> Result<u64> {
1448 let mut w = self.lock_writer()?;
1449 self.publish(&mut w)?;
1450 Ok(self.head_seq())
1451 }
1452
1453 pub fn unsynced(&self) -> u64 {
1455 self.writer.lock().expect("txlog writer").unpublished() as u64
1456 }
1457
1458 fn publish(&self, w: &mut Writer) -> Result<()> {
1460 w.flush_buf()?;
1461 let n = w.unpublished();
1462 if n == 0 {
1463 return Ok(());
1464 }
1465 if let Some(cause) = self.poisoned_by() {
1466 return Err(Error::Poisoned { dir: self.dir.clone(), cause });
1467 }
1468 if let Err(e) = self.sync_frames(&w.file) {
1469 return Err(self.poison_and_cut(w, e));
1470 }
1471 self.frame_syncs.fetch_add(1, Ordering::Relaxed);
1472 self.publish_upto(w, w.seg.offsets.len())
1473 }
1474
1475 pub fn poisoned_by(&self) -> Option<String> {
1477 self.poisoned.lock().unwrap_or_else(|p| p.into_inner()).clone()
1478 }
1479
1480 fn poison(&self, cause: String) {
1482 let mut p = self.poisoned.lock().unwrap_or_else(|p| p.into_inner());
1483 if p.is_none() {
1484 tracing::error!(
1485 target: crate::observe::TARGET_STORE,
1486 event = "txlog.poisoned",
1487 dir = %self.dir.display(),
1488 cause = %cause,
1489 "the transaction log is poisoned: every write refuses until it is reopened"
1490 );
1491 *p = Some(cause);
1492 }
1493 }
1494
1495 fn poison_and_cut(&self, w: &mut Writer, e: io::Error) -> Error {
1499 let mut cause = format!("a frame fsync failed ({e})");
1500 let keep = w.seg.offsets.get(w.published).copied().unwrap_or(w.seg.len);
1501 w.seg.offsets.truncate(w.published);
1502 w.seg.len = keep;
1503 if keep >= w.disk_len {
1504 w.buf.truncate((keep - w.disk_len) as usize);
1505 } else {
1506 w.buf.clear();
1507 let cut = (|| {
1508 w.file.set_len(keep)?;
1509 w.file.seek(SeekFrom::Start(keep))?;
1510 w.disk_len = keep;
1511 w.file.sync_data()
1512 })();
1513 if let Err(c) = cut {
1514 cause.push_str(&format!(", and the frames it did not make durable could not be cut ({c})"));
1515 }
1516 }
1517 self.poison(cause.clone());
1518 Error::Poisoned { dir: self.dir.clone(), cause }
1519 }
1520
1521 pub fn frame_syncs(&self) -> u64 {
1525 self.frame_syncs.load(Ordering::Relaxed)
1526 }
1527
1528 fn publish_upto(&self, w: &mut Writer, upto: usize) -> Result<()> {
1530 if upto <= w.published {
1531 return Ok(());
1532 }
1533 let fresh: Vec<u64> = w.seg.offsets[w.published..upto].to_vec();
1534 let mut bytes = Vec::with_capacity(fresh.len() * 8);
1535 for off in &fresh {
1536 bytes.extend_from_slice(&off.to_le_bytes());
1537 }
1538 w.idx.write_all(&bytes)?;
1541 w.published = upto;
1542 let end = match w.seg.offsets.get(upto) {
1543 Some(&next) => next,
1544 None => w.seg.len,
1545 };
1546 {
1547 let mut segs = self.segments.lock().expect("txlog segments");
1548 let open = segs.last_mut().expect("open segment");
1549 debug_assert_eq!(open.first, w.seg.first, "the published open segment is the writer's");
1550 open.offsets.extend_from_slice(&fresh);
1551 open.len = end;
1552 }
1553 let last = w.seg.first + upto as u64 - 1;
1554 self.head.store(last, Ordering::Release);
1555 Ok(())
1556 }
1557
1558 pub fn begin(&self, seq: Option<u64>, kind: u16, meta: &[u8], blob_len: u64) -> Result<Pending<'_>> {
1563 if seq.is_none() && self.is_follower() {
1564 return Err(Error::Follower);
1565 }
1566 let mut w = self.lock_writer()?;
1567 self.roll_if_full(&mut w)?;
1568 let next = w.seg.first + w.seg.offsets.len() as u64;
1569 if let Some(seq) = seq {
1570 if seq != next {
1571 return Err(Error::OutOfOrder {
1572 got: seq,
1573 expected: next,
1574 });
1575 }
1576 }
1577 let header = Header {
1578 seq: next,
1579 kind,
1580 meta_len: meta.len() as u32,
1581 blob_len,
1582 };
1583 let h = header.encode();
1584 w.put(&h)?;
1585 w.put(meta)?;
1586 let mut crc = Crc32c::new();
1587 crc.update(meta);
1588 Ok(Pending {
1589 log: self,
1590 w,
1591 header,
1592 crc,
1593 written: 0,
1594 finished: false,
1595 })
1596 }
1597
1598 fn lock_writer(&self) -> Result<std::sync::MutexGuard<'_, Writer>> {
1600 let w = self.writer.lock().expect("txlog writer");
1601 if let Some(cause) = self.poisoned_by() {
1602 return Err(Error::Poisoned { dir: self.dir.clone(), cause });
1603 }
1604 if self.released.load(Ordering::Acquire) {
1605 return Err(Error::Released(self.dir.clone()));
1606 }
1607 Ok(w)
1608 }
1609
1610 pub fn release(&self) -> Result<()> {
1622 let mut w = self.writer.lock().expect("txlog writer");
1623 if self.released.load(Ordering::Acquire) {
1624 return Ok(());
1625 }
1626 self.publish(&mut w)?;
1627 w.file.sync_all()?;
1628 w.idx.sync_all()?;
1629 w.file = OpenOptions::new().append(true).open("/dev/null")?;
1630 w.idx = OpenOptions::new().append(true).open("/dev/null")?;
1631 self.released.store(true, Ordering::Release);
1632 Ok(())
1633 }
1634
1635 pub fn reacquire(&self) -> Result<()> {
1639 let mut w = self.writer.lock().expect("txlog writer");
1640 if !self.released.load(Ordering::Acquire) {
1641 return Ok(());
1642 }
1643 let path = seg_path(&self.dir, w.seg.first);
1644 let mut file = OpenOptions::new().append(true).open(&path)?;
1645 let on_disk = file.metadata()?.len();
1646 if on_disk != w.seg.len {
1647 return Err(Error::Corrupt {
1648 path,
1649 offset: on_disk,
1650 what: "the open segment changed length while the log was released",
1651 });
1652 }
1653 file.seek(SeekFrom::Start(w.seg.len))?;
1654 let idx = OpenOptions::new().append(true).open(idx_path(&self.dir, w.seg.first))?;
1655 w.file = file;
1656 w.idx = idx;
1657 w.disk_len = w.seg.len;
1658 self.released.store(false, Ordering::Release);
1659 Ok(())
1660 }
1661
1662 pub fn is_released(&self) -> bool {
1664 self.released.load(Ordering::Acquire)
1665 }
1666
1667 fn roll_if_full(&self, w: &mut Writer) -> Result<()> {
1668 if w.seg.len < self.segment_bytes || w.seg.offsets.is_empty() {
1669 return Ok(());
1670 }
1671 let next_first = w.seg.first + w.seg.offsets.len() as u64;
1672 self.publish(w)?;
1674 w.file.sync_all()?;
1675 w.idx.sync_all()?;
1676 let file = File::create(seg_path(&self.dir, next_first))?;
1677 let idx = File::create(idx_path(&self.dir, next_first))?;
1678 sync_dir(&self.dir)?;
1679 w.file = file;
1680 w.idx = idx;
1681 w.disk_len = 0;
1682 w.published = 0;
1683 w.seg = Segment {
1684 first: next_first,
1685 offsets: Vec::new(),
1686 len: 0,
1687 };
1688 self.segments.lock().expect("txlog segments").push(Segment {
1689 first: next_first,
1690 offsets: Vec::new(),
1691 len: 0,
1692 });
1693 Ok(())
1694 }
1695
1696 fn locate(&self, seq: u64) -> Result<(PathBuf, u64)> {
1698 let head = self.head_seq();
1699 if seq > head {
1700 return Err(Error::BeyondHead { seq, head });
1701 }
1702 let first = self.first_seq();
1703 if seq < first {
1704 return Err(Error::NotRetained { seq, first });
1705 }
1706 let segs = self.segments.lock().expect("txlog segments");
1707 let i = match segs.binary_search_by(|s| s.first.cmp(&seq)) {
1708 Ok(i) => i,
1709 Err(0) => return Err(Error::NotRetained { seq, first }),
1710 Err(i) => i - 1,
1711 };
1712 let seg = &segs[i];
1713 let k = (seq - seg.first) as usize;
1714 let off = *seg.offsets.get(k).ok_or(Error::BeyondHead { seq, head })?;
1715 Ok((seg_path(&self.dir, seg.first), off))
1716 }
1717
1718 pub fn read(&self, seq: u64) -> Result<Entry> {
1720 let (path, off) = self.locate(seq)?;
1721 let mut file = File::open(&path)?;
1722 file.seek(SeekFrom::Start(off))?;
1723 let mut hdr = [0u8; HEADER_LEN];
1724 file.read_exact(&mut hdr)?;
1725 let h = Header::decode(&hdr).ok_or(Error::Corrupt {
1726 path: path.clone(),
1727 offset: off,
1728 what: "header",
1729 })?;
1730 let mut meta = vec![0u8; h.meta_len as usize];
1731 file.read_exact(&mut meta)?;
1732 if h.kind == Kind::Redacted as u16 {
1733 return Ok(Entry { seq: h.seq, kind: h.kind, meta, blob: Vec::new() });
1735 }
1736 let mut blob = vec![0u8; h.blob_len as usize];
1737 file.read_exact(&mut blob)?;
1738 let mut tr = [0u8; TRAILER_LEN];
1739 file.read_exact(&mut tr)?;
1740 let mut crc = Crc32c::new();
1741 crc.update(&meta);
1742 crc.update(&blob);
1743 if crc.finish() != u32::from_le_bytes(tr) {
1744 return Err(Error::Corrupt {
1745 path,
1746 offset: off,
1747 what: "payload crc",
1748 });
1749 }
1750 Ok(Entry {
1751 seq: h.seq,
1752 kind: h.kind,
1753 meta,
1754 blob,
1755 })
1756 }
1757
1758 pub fn locate_blob(&self, seq: u64) -> Result<Located> {
1761 let (path, off) = self.locate(seq)?;
1762 let mut file = File::open(&path)?;
1763 file.seek(SeekFrom::Start(off))?;
1764 let mut hdr = [0u8; HEADER_LEN];
1765 file.read_exact(&mut hdr)?;
1766 let h = Header::decode(&hdr).ok_or(Error::Corrupt {
1767 path: path.clone(),
1768 offset: off,
1769 what: "header",
1770 })?;
1771 let mut meta = vec![0u8; h.meta_len as usize];
1772 file.read_exact(&mut meta)?;
1773 Ok(Located {
1774 seq: h.seq,
1775 kind: h.kind,
1776 meta,
1777 path,
1778 blob_offset: off + HEADER_LEN as u64 + h.meta_len as u64,
1779 blob_len: if h.kind == Kind::Redacted as u16 { 0 } else { h.blob_len },
1781 payload_crc: None,
1782 })
1783 }
1784
1785 pub fn read_pieces<E: From<Error>>(
1799 &self,
1800 seq: u64,
1801 piece: usize,
1802 mut each: impl FnMut(&Located, &[u8], bool) -> std::result::Result<(), E>,
1803 ) -> std::result::Result<Located, E> {
1804 let mut at = self.locate_blob(seq)?;
1805 let frame_at = at.blob_offset - HEADER_LEN as u64 - at.meta.len() as u64;
1806 let corrupt = |path: &Path| Error::Corrupt { path: path.to_path_buf(), offset: frame_at, what: "payload crc" };
1807 if at.kind == Kind::Redacted as u16 {
1808 each(&at, &[], true)?;
1809 return Ok(at);
1810 }
1811 let mut file = File::open(&at.path).map_err(Error::from)?;
1812 file.seek(SeekFrom::Start(at.blob_offset)).map_err(Error::from)?;
1813 let mut crc = Crc32c::new();
1814 crc.update(&at.meta);
1815 let mut buf = vec![0u8; (piece.max(1) as u64).min(at.blob_len) as usize];
1816 let mut left = at.blob_len;
1817 loop {
1818 let n = (buf.len() as u64).min(left) as usize;
1819 file.read_exact(&mut buf[..n]).map_err(Error::from)?;
1820 crc.update(&buf[..n]);
1821 left -= n as u64;
1822 if left > 0 {
1823 each(&at, &buf[..n], false)?;
1824 continue;
1825 }
1826 let mut tr = [0u8; TRAILER_LEN];
1827 file.read_exact(&mut tr).map_err(Error::from)?;
1828 if crc.finish() != u32::from_le_bytes(tr) {
1829 return Err(corrupt(&at.path).into());
1830 }
1831 at.payload_crc = Some(u32::from_le_bytes(tr));
1832 each(&at, &buf[..n], true)?;
1833 return Ok(at);
1834 }
1835 }
1836
1837 pub fn read_pieces_from<E: From<Error>>(
1847 &self,
1848 seq: u64,
1849 from: u64,
1850 piece: usize,
1851 mut each: impl FnMut(&Located, &[u8], bool) -> std::result::Result<(), E>,
1852 ) -> std::result::Result<Located, E> {
1853 if from == 0 {
1854 return self.read_pieces(seq, piece, each);
1855 }
1856 let mut at = self.locate_blob(seq)?;
1857 if at.kind == Kind::Redacted as u16 || from > at.blob_len {
1858 return Err(Error::Io(io::Error::new(
1859 io::ErrorKind::InvalidInput,
1860 format!("txlog: seq {seq} read from byte {from} of a {}-byte blob", at.blob_len),
1861 ))
1862 .into());
1863 }
1864 let mut file = File::open(&at.path).map_err(Error::from)?;
1865 file.seek(SeekFrom::Start(at.blob_offset + from)).map_err(Error::from)?;
1866 let mut left = at.blob_len - from;
1867 let mut buf = vec![0u8; (piece.max(1) as u64).min(left.max(1)) as usize];
1868 loop {
1869 let n = (buf.len() as u64).min(left) as usize;
1870 file.read_exact(&mut buf[..n]).map_err(Error::from)?;
1871 left -= n as u64;
1872 if left > 0 {
1873 each(&at, &buf[..n], false)?;
1874 continue;
1875 }
1876 let mut tr = [0u8; TRAILER_LEN];
1877 file.read_exact(&mut tr).map_err(Error::from)?;
1878 at.payload_crc = Some(u32::from_le_bytes(tr));
1879 each(&at, &buf[..n], true)?;
1880 return Ok(at);
1881 }
1882 }
1883
1884 pub fn erase(&self, before: u64, repos: &[String]) -> Result<Erased> {
1905 let _one = self.erase_lock.lock().unwrap_or_else(|p| p.into_inner());
1906 let plan: Vec<(u64, Vec<u64>)> = {
1910 let mut w = self.lock_writer()?;
1911 self.publish(&mut w)?;
1912 self.segments
1913 .lock()
1914 .expect("txlog segments")
1915 .iter()
1916 .filter(|s| !s.offsets.is_empty() && s.first < before)
1917 .map(|s| (s.first, s.offsets.clone()))
1918 .collect()
1919 };
1920 let data = [Kind::Pack as u16, Kind::Refs as u16, Kind::LfsPut as u16, Kind::LfsDelete as u16];
1921 let mut out = Erased::default();
1922 let mut hdr = [0u8; HEADER_LEN];
1923 for (first, offsets) in plan {
1924 let path = seg_path(&self.dir, first);
1925 let mut file = match OpenOptions::new().read(true).write(true).open(&path) {
1926 Ok(f) => f,
1927 Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
1929 Err(e) => return Err(e.into()),
1930 };
1931 let mut punch: Vec<(u64, u64)> = Vec::new();
1935 for (i, &off) in offsets.iter().enumerate() {
1936 let seq = first + i as u64;
1937 if seq >= before {
1938 break;
1939 }
1940 file.seek(SeekFrom::Start(off))?;
1941 file.read_exact(&mut hdr)?;
1942 let h = Header::decode(&hdr).ok_or(Error::Corrupt { path: path.clone(), offset: off, what: "header" })?;
1943 let redacted_already = h.kind == Kind::Redacted as u16;
1944 if !redacted_already && !data.contains(&h.kind) {
1945 continue;
1946 }
1947 let mut meta = vec![0u8; h.meta_len as usize];
1948 file.read_exact(&mut meta)?;
1949 let named = Meta::from_bytes(&meta).ok().and_then(|m| m.repo().map(str::to_owned));
1950 if !named.is_some_and(|r| repos.contains(&r)) {
1951 continue;
1952 }
1953 let blob_at = off + HEADER_LEN as u64 + h.meta_len as u64;
1954 if redacted_already {
1955 punch.push((blob_at, h.blob_len));
1958 continue;
1959 }
1960 let redacted = Header { kind: Kind::Redacted as u16, ..h };
1961 file.seek(SeekFrom::Start(off))?;
1962 file.write_all(&redacted.encode())?;
1963 punch.push((blob_at, h.blob_len));
1964 out.frames += 1;
1965 out.bytes += h.blob_len;
1966 }
1967 if punch.is_empty() {
1968 continue;
1969 }
1970 file.sync_data()?;
1971 self.erase_syncs.fetch_add(1, Ordering::Relaxed);
1972 #[cfg(test)]
1973 self.erase_gate();
1974 for (at, len) in punch {
1976 zero_range(&mut file, at, len)?;
1977 }
1978 file.sync_all()?;
1979 self.erase_syncs.fetch_add(1, Ordering::Relaxed);
1980 }
1981 Ok(out)
1982 }
1983
1984 pub fn erase_syncs(&self) -> u64 {
1986 self.erase_syncs.load(Ordering::Relaxed)
1987 }
1988
1989 #[cfg(test)]
1991 fn hold_next_erase(&self, gate: std::sync::mpsc::Receiver<()>) {
1992 self.erase_gates.lock().unwrap().push_back(gate);
1993 }
1994
1995 #[cfg(test)]
1996 fn erase_gate(&self) {
1997 let gate = self.erase_gates.lock().unwrap_or_else(|p| p.into_inner()).pop_front();
1998 if let Some(gate) = gate {
1999 let _ = gate.recv();
2000 }
2001 }
2002
2003 pub fn scan_meta(&self, mut each: impl FnMut(u64, u16, &[u8])) -> Result<()> {
2012 let head = self.head_seq();
2013 let plan: Vec<(u64, u64, u64)> = self
2014 .segments
2015 .lock()
2016 .expect("txlog segments")
2017 .iter()
2018 .filter(|s| !s.offsets.is_empty())
2019 .map(|s| (s.first, s.offsets[0], s.offsets.len() as u64))
2020 .collect();
2021 let mut hdr = [0u8; HEADER_LEN];
2022 let mut meta: Vec<u8> = Vec::with_capacity(256);
2023 for (first, start, n) in plan {
2024 let path = seg_path(&self.dir, first);
2025 let mut r = io::BufReader::with_capacity(1 << 16, File::open(&path)?);
2026 r.seek(SeekFrom::Start(start))?;
2027 let mut offset = start;
2028 for seq in first..first + n {
2029 if seq > head {
2030 return Ok(());
2031 }
2032 r.read_exact(&mut hdr)?;
2033 let h = match Header::decode(&hdr) {
2034 Some(h) if h.seq == seq => h,
2035 Some(_) => return Err(Error::Corrupt { path, offset, what: "sequence out of place" }),
2036 None => return Err(Error::Corrupt { path, offset, what: "header" }),
2037 };
2038 meta.resize(h.meta_len as usize, 0);
2039 r.read_exact(&mut meta)?;
2040 each(seq, h.kind, &meta);
2041 r.seek_relative((h.blob_len + TRAILER_LEN as u64) as i64)?;
2042 offset += h.frame_len();
2043 }
2044 }
2045 Ok(())
2046 }
2047
2048 pub fn range(&self, from: u64, to: u64) -> Result<Vec<Entry>> {
2050 let to = to.min(self.head_seq());
2051 let mut out = Vec::new();
2052 let mut seq = from;
2053 while seq <= to {
2054 out.push(self.read(seq)?);
2055 seq += 1;
2056 }
2057 Ok(out)
2058 }
2059
2060 pub fn truncate_before(&self, seq: u64) -> Result<u64> {
2063 let _w = self.lock_writer()?;
2064 let mut segs = self.segments.lock().expect("txlog segments");
2065 while segs.len() > 1 {
2066 let drop = match segs[0].last_seq() {
2067 Some(last) => last < seq,
2068 None => true,
2069 };
2070 if !drop {
2071 break;
2072 }
2073 let first = segs[0].first;
2074 fs::remove_file(seg_path(&self.dir, first))?;
2075 let _ = fs::remove_file(idx_path(&self.dir, first));
2076 segs.remove(0);
2077 }
2078 sync_dir(&self.dir)?;
2079 let first = segs[0].first;
2080 self.first.store(first, Ordering::Release);
2081 Ok(first)
2082 }
2083
2084 pub fn retain_from(&self, seq: u64) -> Result<u64> {
2090 {
2091 let mut w = self.writer.lock().expect("txlog writer");
2092 self.publish(&mut w)?;
2093 if !w.seg.offsets.is_empty() {
2094 let next_first = w.seg.first + w.seg.offsets.len() as u64;
2095 w.file.sync_all()?;
2096 w.idx.sync_all()?;
2097 let file = File::create(seg_path(&self.dir, next_first))?;
2098 let idx = File::create(idx_path(&self.dir, next_first))?;
2099 sync_dir(&self.dir)?;
2100 w.file = file;
2101 w.idx = idx;
2102 w.disk_len = 0;
2103 w.published = 0;
2104 w.seg = Segment { first: next_first, offsets: Vec::new(), len: 0 };
2105 self.segments.lock().expect("txlog segments").push(Segment { first: next_first, offsets: Vec::new(), len: 0 });
2106 }
2107 }
2108 self.truncate_before(seq)
2109 }
2110
2111 pub fn reset(&self, keep_at_most: u64) -> Result<()> {
2117 let head = self.head_seq();
2118 if head > keep_at_most {
2119 return Err(Error::OutOfOrder {
2120 got: head,
2121 expected: keep_at_most,
2122 });
2123 }
2124 self.restart_after(0)
2125 }
2126
2127 pub fn restart_after(&self, at: u64) -> Result<()> {
2131 let mut w = self.lock_writer()?;
2132 let mut segs = self.segments.lock().expect("txlog segments");
2133 for s in segs.iter() {
2134 fs::remove_file(seg_path(&self.dir, s.first))?;
2135 let _ = fs::remove_file(idx_path(&self.dir, s.first));
2136 }
2137 segs.clear();
2138 let first = at + 1;
2139 File::create(seg_path(&self.dir, first))?;
2140 File::create(idx_path(&self.dir, first))?;
2141 sync_dir(&self.dir)?;
2142 segs.push(Segment {
2143 first,
2144 offsets: Vec::new(),
2145 len: 0,
2146 });
2147 w.file = OpenOptions::new().append(true).open(seg_path(&self.dir, first))?;
2148 w.idx = OpenOptions::new().append(true).open(idx_path(&self.dir, first))?;
2149 w.seg = Segment {
2150 first,
2151 offsets: Vec::new(),
2152 len: 0,
2153 };
2154 w.disk_len = 0;
2155 w.buf.clear();
2156 w.published = 0;
2157 self.head.store(at, Ordering::Release);
2158 self.first.store(first, Ordering::Release);
2159 Ok(())
2160 }
2161
2162 pub fn layout(&self) -> Vec<(u64, u64, u64)> {
2168 self.segments
2169 .lock()
2170 .expect("txlog segments")
2171 .iter()
2172 .map(|s| (s.first, s.first + s.offsets.len() as u64 - 1, s.len))
2173 .collect()
2174 }
2175
2176 pub fn bytes(&self) -> u64 {
2178 self.segments
2179 .lock()
2180 .expect("txlog segments")
2181 .iter()
2182 .map(|s| s.len)
2183 .sum()
2184 }
2185}
2186
2187pub struct Pending<'a> {
2197 log: &'a Log,
2198 w: std::sync::MutexGuard<'a, Writer>,
2199 header: Header,
2200 crc: Crc32c,
2201 written: u64,
2202 finished: bool,
2204}
2205
2206impl Pending<'_> {
2207 pub fn seq(&self) -> u64 {
2209 self.header.seq
2210 }
2211
2212 pub fn remaining(&self) -> u64 {
2214 self.header.blob_len - self.written
2215 }
2216
2217 pub fn write(&mut self, chunk: &[u8]) -> Result<()> {
2219 if chunk.len() as u64 > self.remaining() {
2220 return Err(Error::Io(io::Error::new(
2221 io::ErrorKind::InvalidInput,
2222 "txlog: chunk exceeds the declared blob length",
2223 )));
2224 }
2225 self.w.put(chunk)?;
2226 self.crc.update(chunk);
2227 self.written += chunk.len() as u64;
2228 Ok(())
2229 }
2230
2231 pub fn commit(mut self) -> Result<u64> {
2234 let seq = self.close()?;
2235 self.log.publish(&mut self.w)?;
2236 Ok(seq)
2237 }
2238
2239 pub fn commit_unsynced(mut self) -> Result<u64> {
2249 self.close()
2250 }
2251
2252 fn close(&mut self) -> Result<u64> {
2254 if self.remaining() != 0 {
2255 let owed = self.remaining();
2256 self.abort_inner()?;
2257 return Err(Error::Io(io::Error::new(
2258 io::ErrorKind::UnexpectedEof,
2259 format!("txlog: blob short by {owed} bytes at commit"),
2260 )));
2261 }
2262 let tr = self.crc.finish().to_le_bytes();
2263 self.w.put(&tr)?;
2264 self.finished = true;
2266 let off = self.w.seg.len;
2267 self.w.seg.offsets.push(off);
2268 self.w.seg.len += self.header.frame_len();
2269 Ok(self.header.seq)
2270 }
2271
2272 pub fn abort(mut self) -> Result<()> {
2274 self.abort_inner()
2275 }
2276
2277 fn abort_inner(&mut self) -> Result<()> {
2278 self.finished = true;
2279 if let Err(e) = self.w.cut_to_committed() {
2280 self.log.poison(format!("a partial frame could not be cut off the segment ({e})"));
2282 return Err(e.into());
2283 }
2284 Ok(())
2285 }
2286}
2287
2288impl Drop for Pending<'_> {
2289 fn drop(&mut self) {
2290 if self.finished {
2291 return;
2292 }
2293 let _ = self.abort_inner();
2296 }
2297}
2298
2299pub fn is_cache_path(rel: &str) -> bool {
2305 let rel = rel.trim_start_matches("./");
2306 let rel = rel.strip_prefix(".serialdat/").or_else(|| rel.strip_prefix("serialdat/")).unwrap_or(rel);
2309 let first = rel.split('/').next().unwrap_or("");
2310 let name = rel.rsplit('/').next().unwrap_or("");
2311 matches!(first, ".korp-disktest" | "gc" | "quarantine" | "meta" | "serialdat")
2312 || rel.split('/').any(|c| c.starts_with("objects.exploded"))
2313 || name == "objects.graph"
2315 || name.ends_with(".tmp")
2316 || name.starts_with("incoming-")
2317 || name == "grow-journal"
2318}
2319
2320pub const AUDIT_REL: &str = "log/audit-chain.jsonl";
2327
2328pub const AUDIT_LOCAL_REL: &str = "log/audit-chain.local.jsonl";
2341
2342pub const HUB_CONF_REL: &str = "wg/hub.conf";
2353
2354pub const HUB_CONF_FRAMED_REL: &str = "wg/hub.conf.framed";
2358
2359pub const WG_IDENTITY_KEY_REL: &str = "wg/identity.key";
2370pub const WG_IDENTITY_PUB_REL: &str = "wg/identity.pub";
2372
2373pub fn is_per_box_path(rel: &str) -> bool {
2383 let rel = rel.trim_start_matches("./");
2384 let rel = rel.strip_prefix(".serialdat/").or_else(|| rel.strip_prefix("serialdat/")).unwrap_or(rel);
2387 if rel == AUDIT_REL || rel == HUB_CONF_REL {
2388 return false;
2389 }
2390 if matches!(rel, WG_IDENTITY_KEY_REL | WG_IDENTITY_PUB_REL | HUB_CONF_FRAMED_REL) {
2393 return true;
2394 }
2395 let first = rel.split('/').next().unwrap_or("");
2396 matches!(
2399 first,
2400 "crash" | "log" | "twin.json" | "twin.key" | "txlog-restoring" | "txlog-fresh-docs.json" | "txlog-reseal" | "txlog-snapshot-installing" | "txlog-snapshot-base" | "txlog-snapshot-base.old"
2401 | "txlog-snapshot-dropped" | "txlog-snapshot-too-small"
2402 | "txlog-restore-partial"
2404 | "txlog-restore-snapshot"
2406 )
2407}
2408
2409pub const FRAMED_FILES: &[&str] = &[
2414 "attestations.jsonl",
2415 "transparency-heads.jsonl",
2416 "builds.jsonl",
2417 "build-heads.jsonl",
2418 "forge.jsonl",
2419 "forge-heads.jsonl",
2420 "membership.jsonl",
2421 "membership-heads.jsonl",
2422 "replication.json",
2423 "webhooks.json",
2424 AUDIT_REL,
2427 HUB_CONF_REL,
2430];
2431
2432pub fn never_framed(rel: &str) -> bool {
2435 is_cache_path(rel) || is_per_box_path(rel)
2436}
2437
2438pub mod sink {
2448 use super::*;
2449
2450 static ROOTS: Mutex<Vec<(PathBuf, Weak<Log>)>> = Mutex::new(Vec::new());
2451
2452 fn refuse_cache(rel: &str) -> Result<()> {
2453 if never_framed(rel) {
2454 return Err(Error::BadMeta(format!("{rel} is a cache or per-box path; it is never a transaction")));
2455 }
2456 Ok(())
2457 }
2458
2459 pub fn register(root: &Path, log: &Arc<Log>) {
2461 let mut roots = ROOTS.lock().expect("txlog sink");
2462 roots.retain(|(r, w)| w.strong_count() > 0 && r != root);
2463 roots.push((root.to_path_buf(), Arc::downgrade(log)));
2464 }
2465
2466 pub fn log_for(path: &Path) -> Option<(Arc<Log>, String)> {
2468 let roots = ROOTS.lock().expect("txlog sink");
2469 roots
2470 .iter()
2471 .filter(|(r, _)| path.starts_with(r))
2472 .max_by_key(|(r, _)| r.as_os_str().len())
2473 .and_then(|(r, w)| {
2474 let log = w.upgrade()?;
2475 let rel = path.strip_prefix(r).ok()?.to_string_lossy().into_owned();
2476 Some((log, rel))
2477 })
2478 }
2479
2480 #[must_use = "hold the frame until the file has the write"]
2483 pub struct Framed(#[allow(dead_code)] Option<MutationGuard>);
2484
2485 pub fn append_line(path: &Path, line: &str) -> Result<Framed> {
2487 if let Some((log, rel)) = log_for(path) {
2488 refuse_cache(&rel)?;
2489 let guard = log.mutation();
2490 log.append(Kind::Append, &Meta::Append { log: rel }, line.as_bytes())?;
2491 return Ok(Framed(Some(guard)));
2492 }
2493 Ok(Framed(None))
2494 }
2495
2496 #[must_use = "wait for the frame to be durable, then write the line, then drop this"]
2499 pub struct StagedLine {
2500 _guard: MutationGuard,
2501 log: Arc<Log>,
2502 seq: u64,
2503 }
2504
2505 impl StagedLine {
2506 pub fn seq(&self) -> u64 {
2508 self.seq
2509 }
2510 pub fn durable(&self) -> Result<()> {
2513 self.log.sync_through(self.seq)
2514 }
2515 pub fn log(&self) -> &Arc<Log> {
2517 &self.log
2518 }
2519 }
2520
2521 pub fn stage_line(path: &Path, line: &str) -> Result<Option<StagedLine>> {
2525 if let Some((log, rel)) = log_for(path) {
2526 refuse_cache(&rel)?;
2527 if log.is_follower() {
2528 return Err(Error::Follower);
2529 }
2530 let guard = log.mutation();
2531 let seq = log.stage_raw(Kind::Append as u16, &Meta::Append { log: rel }.to_bytes(), line.as_bytes())?;
2532 return Ok(Some(StagedLine { _guard: guard, log, seq }));
2533 }
2534 Ok(None)
2535 }
2536
2537 pub fn replace_file(path: &Path, bytes: &[u8], deleted: bool) -> Result<Framed> {
2539 if let Some((log, rel)) = log_for(path) {
2540 refuse_cache(&rel)?;
2541 let guard = log.mutation();
2542 log.append(Kind::File, &Meta::File { path: rel, deleted }, bytes)?;
2543 return Ok(Framed(Some(guard)));
2544 }
2545 Ok(Framed(None))
2546 }
2547}
2548
2549#[cfg(test)]
2550mod tests {
2551 use super::*;
2552
2553 fn meta(repo: &str) -> Meta {
2554 Meta::Pack {
2555 repo: repo.into(),
2556 hash: "sha256".into(),
2557 }
2558 }
2559
2560 #[test]
2566 fn a_failed_fsync_poisons_the_log_until_it_is_reopened() {
2567 let dir = tempfile::tempdir().unwrap();
2568 let m = meta("g/r").to_bytes();
2569 {
2570 let log = Log::open(dir.path()).unwrap();
2571 assert_eq!(log.append_raw(Kind::Pack as u16, &m, b"one").unwrap(), 1);
2572 log.fail_next_fsyncs(1);
2573 log.append_raw(Kind::Pack as u16, &m, b"two").expect_err("its fsync failed");
2574 let next = log.append_raw(Kind::Pack as u16, &m, b"three");
2575 assert!(next.is_err(), "an append after a failed fsync was taken (and published the failed frame with it): {next:?}");
2576 assert!(log.sync().is_err() || log.head_seq() == 1, "a sync published past the failed fsync");
2577 assert_eq!(log.head_seq(), 1, "the frame whose fsync failed was published");
2578 assert!(log.read(2).is_err(), "the frame whose fsync failed is readable");
2579 }
2580 let log = Log::open(dir.path()).unwrap();
2581 assert_eq!(log.head_seq(), 1, "a reopen resurrected the frame whose fsync failed");
2582 assert_eq!(log.append_raw(Kind::Pack as u16, &m, b"again").unwrap(), 2);
2583 assert_eq!(log.read(2).unwrap().blob, b"again");
2584 }
2585
2586 #[test]
2591 fn a_failed_cut_of_a_half_frame_poisons_the_log() {
2592 let dir = tempfile::tempdir().unwrap();
2593 let m = meta("g/r").to_bytes();
2594 {
2595 let log = Log::open(dir.path()).unwrap();
2596 assert_eq!(log.append_raw(Kind::Pack as u16, &m, b"one").unwrap(), 1);
2597 {
2598 let mut w = log.writer.lock().unwrap();
2599 w.faults.fail_put_after = Some(3);
2601 w.faults.fail_cut = true;
2602 }
2603 let big = vec![7u8; DIRECT_BYTES * 2];
2604 log.append_raw(Kind::Pack as u16, &m, &big).expect_err("its trailer failed");
2605 let next = log.append_raw(Kind::Pack as u16, &m, b"three");
2606 assert!(next.is_err(), "an append after a failed cut landed after the half frame: {next:?}");
2607 assert_eq!(log.head_seq(), 1);
2608 }
2609 let log = Log::open(dir.path()).unwrap();
2610 assert_eq!(log.head_seq(), 1);
2611 assert_eq!(log.append_raw(Kind::Pack as u16, &m, b"three").unwrap(), 2);
2612 assert_eq!(log.read(2).unwrap().blob, b"three");
2613 }
2614
2615 #[test]
2616 fn crc32c_known_answer() {
2617 assert_eq!(crc32c(b"123456789"), 0xE306_9283);
2619 let mut a = Crc32c::new();
2621 a.update(b"1234");
2622 let mut b = Crc32c::resume(a.state());
2623 b.update(b"56789");
2624 assert_eq!(b.finish(), 0xE306_9283);
2625 }
2626
2627 #[test]
2628 fn appends_are_dense_and_read_back() {
2629 let dir = tempfile::tempdir().unwrap();
2630 let log = Log::open(dir.path()).unwrap();
2631 assert_eq!(log.head_seq(), 0);
2632 assert_eq!(log.first_seq(), 1);
2633 for i in 1..=10u64 {
2634 let s = log
2635 .append(Kind::Pack, &meta("a/b"), &[i as u8; 100])
2636 .unwrap();
2637 assert_eq!(s, i);
2638 }
2639 assert_eq!(log.head_seq(), 10);
2640 let e = log.read(7).unwrap();
2641 assert_eq!(e.seq, 7);
2642 assert_eq!(e.kind(), Some(Kind::Pack));
2643 assert_eq!(e.blob, vec![7u8; 100]);
2644 assert_eq!(e.meta().unwrap(), meta("a/b"));
2645 assert!(matches!(log.read(11), Err(Error::BeyondHead { .. })));
2646 assert!(matches!(log.read(0), Err(Error::NotRetained { .. })));
2647 }
2648
2649 #[test]
2650 fn reopen_keeps_head_and_trusts_index() {
2651 let dir = tempfile::tempdir().unwrap();
2652 {
2653 let log = Log::open(dir.path()).unwrap();
2654 for i in 0..5u64 {
2655 log.append(Kind::Refs, &meta("x/y"), &[i as u8]).unwrap();
2656 }
2657 }
2658 let log = Log::open(dir.path()).unwrap();
2659 assert_eq!(log.head_seq(), 5);
2660 assert_eq!(log.read(5).unwrap().blob, vec![4u8]);
2661 assert_eq!(log.append(Kind::Refs, &meta("x/y"), b"6").unwrap(), 6);
2662 }
2663
2664 #[test]
2665 fn torn_tail_is_cut_and_the_log_goes_on() {
2666 let dir = tempfile::tempdir().unwrap();
2667 {
2668 let log = Log::open(dir.path()).unwrap();
2669 log.append(Kind::Pack, &meta("a/b"), &[1u8; 1000]).unwrap();
2670 log.append(Kind::Pack, &meta("a/b"), &[2u8; 1000]).unwrap();
2671 }
2672 let path = seg_path(dir.path(), 1);
2673 let len = fs::metadata(&path).unwrap().len();
2674 let f = OpenOptions::new().write(true).open(&path).unwrap();
2676 f.set_len(len - 100).unwrap();
2677 let log = Log::open(dir.path()).unwrap();
2678 assert_eq!(log.head_seq(), 1, "the torn second frame is gone");
2679 assert_eq!(fs::metadata(&path).unwrap().len(), len - 1000 - 32 - 4 - meta("a/b").to_bytes().len() as u64);
2680 assert_eq!(log.append(Kind::Pack, &meta("a/b"), &[3u8; 10]).unwrap(), 2);
2681 drop(log);
2682 let log = Log::open(dir.path()).unwrap();
2683 assert_eq!(log.read(2).unwrap().blob, vec![3u8; 10]);
2684 }
2685
2686 fn unsynced(log: &Log, seq: u64, blob: &[u8]) -> u64 {
2688 let m = meta("g/r").to_bytes();
2689 let mut p = log.begin(Some(seq), Kind::Pack as u16, &m, blob.len() as u64).unwrap();
2690 p.write(blob).unwrap();
2691 p.commit_unsynced().unwrap()
2692 }
2693
2694 #[test]
2699 fn a_group_is_invisible_until_synced_and_then_durable_as_one() {
2700 let dir = tempfile::tempdir().unwrap();
2701 let log = Log::open(dir.path()).unwrap();
2702 log.set_follower(true);
2703 for seq in 1..=5u64 {
2704 assert_eq!(unsynced(&log, seq, &[seq as u8; 100]), seq);
2705 }
2706 assert_eq!(log.head_seq(), 0, "nothing acknowledged before the sync");
2707 assert_eq!(log.unsynced(), 5);
2708 assert!(matches!(log.read(1), Err(Error::BeyondHead { .. })), "a reader does not see a frame that is not durable");
2709 assert!(matches!(log.begin(Some(5), Kind::Pack as u16, b"{}", 0), Err(Error::OutOfOrder { got: 5, expected: 6 })), "the gap rule counts the group");
2710 assert_eq!(fs::metadata(idx_path(dir.path(), 1)).unwrap().len(), 0, "the index is written after the data's fsync");
2711 assert_eq!(log.sync().unwrap(), 5);
2712 assert_eq!((log.head_seq(), log.unsynced()), (5, 0));
2713 assert_eq!(log.read(3).unwrap().blob, vec![3u8; 100]);
2714 assert_eq!(fs::metadata(idx_path(dir.path(), 1)).unwrap().len(), 5 * 8, "one index entry per frame");
2715 assert_eq!(log.sync().unwrap(), 5, "nothing pending: nothing done");
2716 drop(log);
2717 let log = Log::open(dir.path()).unwrap();
2718 assert_eq!(log.head_seq(), 5);
2719 assert_eq!(log.read(5).unwrap().blob, vec![5u8; 100]);
2720 }
2721
2722 #[test]
2727 fn a_crash_before_the_sync_loses_only_what_was_never_acknowledged() {
2728 let dir = tempfile::tempdir().unwrap();
2729 let log = Log::open(dir.path()).unwrap();
2730 for seq in 1..=3u64 {
2731 unsynced(&log, seq, b"acked");
2732 }
2733 assert_eq!(log.sync().unwrap(), 3);
2734 unsynced(&log, 4, b"staged");
2736 unsynced(&log, 5, b"staged");
2737 drop(log);
2738 let log = Log::open(dir.path()).unwrap();
2739 assert_eq!(log.head_seq(), 3, "the acknowledged group is whole; the staged one never reached the file");
2740 let big = vec![7u8; DIRECT_BYTES * 2];
2742 unsynced(&log, 4, &big);
2743 let m = meta("g/r").to_bytes();
2744 let mut half = log.begin(Some(5), Kind::Pack as u16, &m, (DIRECT_BYTES * 4) as u64).unwrap();
2745 half.write(&big).unwrap();
2746 std::mem::forget(half); let path = seg_path(dir.path(), 1);
2748 let on_disk = fs::metadata(&path).unwrap().len();
2749 drop(log);
2750 let log = Log::open(dir.path()).unwrap();
2751 assert_eq!(log.head_seq(), 4, "the whole frame that reached the file is kept, the half one is cut");
2752 assert_eq!(log.read(4).unwrap().blob, big);
2753 assert!(fs::metadata(&path).unwrap().len() < on_disk, "the half frame is gone from the segment");
2754 assert_eq!(log.append(Kind::Pack, &meta("g/r"), b"next").unwrap(), 5);
2755 }
2756
2757 #[test]
2760 fn an_abort_inside_a_group_keeps_the_frames_committed_before_it() {
2761 let dir = tempfile::tempdir().unwrap();
2762 let log = Log::open(dir.path()).unwrap();
2763 unsynced(&log, 1, b"one");
2764 unsynced(&log, 2, &vec![2u8; DIRECT_BYTES]);
2765 let m = meta("g/r").to_bytes();
2766 {
2767 let mut p = log.begin(Some(3), Kind::Pack as u16, &m, 10).unwrap();
2768 p.write(b"abc").unwrap();
2769 }
2771 {
2772 let mut p = log.begin(Some(3), Kind::Pack as u16, &m, (DIRECT_BYTES * 2) as u64).unwrap();
2773 p.write(&vec![9u8; DIRECT_BYTES]).unwrap();
2774 p.abort().unwrap();
2775 }
2776 assert_eq!(log.sync().unwrap(), 2);
2777 assert_eq!(log.read(2).unwrap().blob, vec![2u8; DIRECT_BYTES]);
2778 assert_eq!(unsynced(&log, 3, b"three"), 3);
2779 assert_eq!(log.sync().unwrap(), 3);
2780 drop(log);
2781 let log = Log::open(dir.path()).unwrap();
2782 assert_eq!(log.head_seq(), 3);
2783 assert_eq!(log.read(3).unwrap().blob, b"three".to_vec());
2784 }
2785
2786 #[test]
2801 fn a_waiter_whose_frame_is_durable_returns_without_waiting_for_the_next_leaders_fsync() {
2802 use std::sync::mpsc::channel;
2803 use std::time::Duration;
2804 let dir = tempfile::tempdir().unwrap();
2805 let log = Arc::new(Log::open(dir.path()).unwrap());
2806 let m = meta("g/r").to_bytes();
2807 assert_eq!(log.stage_raw(Kind::Pack as u16, &m, b"one").unwrap(), 1);
2808 assert_eq!(log.stage_raw(Kind::Pack as u16, &m, b"x").unwrap(), 2);
2809 let (open_a, gate_a) = channel::<()>();
2810 let (open_b, gate_b) = channel::<()>();
2811 log.hold_next_fsync(gate_a);
2812 log.hold_next_fsync(gate_b);
2813 let gates_left = |log: &Log| log.fsync_gates.lock().unwrap().len();
2814
2815 let a = { let log = Arc::clone(&log); std::thread::spawn(move || log.sync_through(1).unwrap()) };
2817 while gates_left(&log) != 1 {
2818 std::thread::sleep(Duration::from_millis(5));
2819 }
2820 assert_eq!(log.stage_raw(Kind::Pack as u16, &m, b"three").unwrap(), 3);
2822 let b = { let log = Arc::clone(&log); std::thread::spawn(move || log.sync_through(3).unwrap()) };
2823 std::thread::sleep(Duration::from_millis(200));
2824 let (x_done, x_said) = channel::<()>();
2825 let x = { let log = Arc::clone(&log); std::thread::spawn(move || { log.sync_through(2).unwrap(); let _ = x_done.send(()); }) };
2826 std::thread::sleep(Duration::from_millis(200));
2827
2828 open_a.send(()).unwrap();
2830 a.join().unwrap();
2831 assert!(log.head_seq() >= 2, "A published frames 1 and 2");
2832 let t = std::time::Instant::now();
2834 while gates_left(&log) != 0 && t.elapsed() < Duration::from_secs(5) {
2835 std::thread::sleep(Duration::from_millis(5));
2836 }
2837 let held = gates_left(&log) == 0;
2838 let back = x_said.recv_timeout(Duration::from_secs(5));
2839 open_b.send(()).unwrap();
2840 b.join().unwrap();
2841 x.join().unwrap();
2842 assert!(held, "B led frame 3's fsync (its gate was taken)");
2843 assert!(back.is_ok(), "X's frame was durable since A published it, yet X waited behind B's fsync of frame 3");
2844 assert_eq!(log.head_seq(), 3);
2845 }
2846
2847 #[test]
2855 fn an_erase_is_two_fsyncs_per_segment_and_appends_do_not_wait_for_it() {
2856 use std::sync::mpsc::channel;
2857 use std::time::Duration;
2858 let dir = tempfile::tempdir().unwrap();
2859 let log = Arc::new(Log::open_with(dir.path(), 64 << 10).unwrap());
2860 for i in 0..200u64 {
2861 let repo = if i % 2 == 0 { "gone/a" } else { "stays/b" };
2862 log.append(Kind::Pack, &meta(repo), &vec![i as u8; 1500]).unwrap();
2863 log.append(Kind::Pack, &meta("gone/a"), &vec![7u8; 600]).unwrap();
2864 }
2865 let before = log.append(Kind::Refs, &meta("stays/b"), b"marker").unwrap() ;
2866 let segments = log.segments.lock().unwrap().len() as u64;
2867 let syncs0 = log.erase_syncs();
2868
2869 let (open, gate) = channel::<()>();
2871 log.hold_next_erase(gate);
2872 let eraser = { let log = Arc::clone(&log); std::thread::spawn(move || log.erase(before, &["gone/a".to_string()]).unwrap()) };
2873 while !log.erase_gates.lock().unwrap().is_empty() {
2874 std::thread::sleep(Duration::from_millis(5));
2875 }
2876 let (done, said) = channel::<u64>();
2877 let appender = { let log = Arc::clone(&log); std::thread::spawn(move || { let s = log.append(Kind::Refs, &meta("stays/b"), b"during").unwrap(); let _ = done.send(s); }) };
2878 let appended = said.recv_timeout(Duration::from_secs(3));
2879 open.send(()).unwrap();
2880 let erased = eraser.join().unwrap();
2881 appender.join().unwrap();
2882 let syncs = log.erase_syncs() - syncs0;
2883 let line = format!("erased {} frames in {segments} segments with {syncs} fsyncs", erased.frames);
2884 eprintln!("{line}");
2885 assert_eq!(erased.frames, 300, "{line}");
2886 assert!(appended.is_ok(), "an append waited for the erase: {line}");
2887 assert!(syncs <= 2 * segments, "about two fsyncs per segment: {line}");
2888 for seq in 1..before {
2890 let e = log.read(seq).unwrap();
2891 let gone = Meta::from_bytes(&e.meta).unwrap().repo() == Some("gone/a");
2892 assert_eq!(e.kind == Kind::Redacted as u16, gone, "seq {seq}");
2893 assert_eq!(e.blob.is_empty(), gone, "seq {seq}");
2894 }
2895 }
2896
2897 #[test]
2902 fn a_group_across_a_segment_roll_and_a_synced_append_after_it() {
2903 let dir = tempfile::tempdir().unwrap();
2904 let log = Log::open_with(dir.path(), 4096).unwrap();
2905 for seq in 1..=40u64 {
2906 unsynced(&log, seq, &vec![seq as u8; 300]);
2907 }
2908 assert!(log.head_seq() > 0, "the rolls published the groups in the closed segments");
2909 assert_eq!(log.sync().unwrap(), 40);
2910 unsynced(&log, 41, b"group");
2911 assert_eq!(log.append(Kind::Pack, &meta("g/r"), b"synced").unwrap(), 42, "the append comes after the group");
2912 assert_eq!(log.head_seq(), 42);
2913 for seq in 1..=40u64 {
2914 assert_eq!(log.read(seq).unwrap().blob, vec![seq as u8; 300], "seq {seq}");
2915 }
2916 drop(log);
2917 let log = Log::open_with(dir.path(), 4096).unwrap();
2918 assert_eq!(log.head_seq(), 42);
2919 assert_eq!(log.read(41).unwrap().blob, b"group".to_vec());
2920 assert_eq!(log.read(42).unwrap().blob, b"synced".to_vec());
2921 }
2922
2923 #[test]
2924 fn torn_header_is_cut_too() {
2925 let dir = tempfile::tempdir().unwrap();
2926 {
2927 let log = Log::open(dir.path()).unwrap();
2928 log.append(Kind::Pack, &meta("a/b"), &[1u8; 10]).unwrap();
2929 }
2930 let path = seg_path(dir.path(), 1);
2931 let mut f = OpenOptions::new().append(true).open(&path).unwrap();
2932 f.write_all(b"GTX1\x05\x00\x00").unwrap(); let log = Log::open(dir.path()).unwrap();
2934 assert_eq!(log.head_seq(), 1);
2935 assert_eq!(log.append(Kind::Pack, &meta("a/b"), &[2u8; 10]).unwrap(), 2);
2936 }
2937
2938 #[test]
2941 fn scan_meta_agrees_with_locate_blob_across_segments_and_retention() {
2942 let dir = tempfile::tempdir().unwrap();
2943 let log = Log::open_with(dir.path(), 4096).unwrap();
2944 for i in 0..200u64 {
2945 let blob = vec![i as u8; (i as usize * 37) % 9000];
2946 log.append(Kind::Pack, &meta(&format!("r/{i}")), &blob).unwrap();
2947 }
2948 log.retain_from(50).unwrap();
2949 let mut seen = Vec::new();
2950 log.scan_meta(|seq, kind, m| seen.push((seq, kind, m.to_vec()))).unwrap();
2951 let want: Vec<_> = (log.first_seq()..=log.head_seq()).map(|s| { let at = log.locate_blob(s).unwrap(); (at.seq, at.kind, at.meta) }).collect();
2952 assert!(log.first_seq() > 1 && !want.is_empty());
2953 assert_eq!(seen, want);
2954 }
2955
2956 #[test]
2957 fn corrupted_payload_in_tail_is_cut() {
2958 let dir = tempfile::tempdir().unwrap();
2959 let first_len;
2960 {
2961 let log = Log::open(dir.path()).unwrap();
2962 log.append(Kind::Pack, &meta("a/b"), &[1u8; 10]).unwrap();
2963 first_len = log.bytes();
2964 log.append(Kind::Pack, &meta("a/b"), &[2u8; 10]).unwrap();
2965 }
2966 let path = seg_path(dir.path(), 1);
2967 let mut f = OpenOptions::new().write(true).open(&path).unwrap();
2968 f.seek(SeekFrom::Start(first_len + 32 + meta("a/b").to_bytes().len() as u64 + 3)).unwrap();
2970 f.write_all(&[0xEE]).unwrap();
2971 let log = Log::open(dir.path()).unwrap();
2972 assert_eq!(log.head_seq(), 1);
2973 }
2974
2975 #[test]
2976 fn stale_index_is_rebuilt_from_data() {
2977 let dir = tempfile::tempdir().unwrap();
2978 {
2979 let log = Log::open(dir.path()).unwrap();
2980 for _ in 0..4 {
2981 log.append(Kind::Pack, &meta("a/b"), &[9u8; 10]).unwrap();
2982 }
2983 }
2984 fs::remove_file(idx_path(dir.path(), 1)).unwrap();
2986 fs::write(idx_path(dir.path(), 1), []).unwrap();
2987 let log = Log::open(dir.path()).unwrap();
2988 assert_eq!(log.head_seq(), 4);
2989 drop(log);
2990 fs::write(idx_path(dir.path(), 1), [0u8; 8 * 9]).unwrap();
2991 let log = Log::open(dir.path()).unwrap();
2992 assert_eq!(log.head_seq(), 4);
2993 assert_eq!(log.read(3).unwrap().seq, 3);
2994 }
2995
2996 #[test]
2997 fn segments_roll_and_retention_drops_whole_segments() {
2998 let dir = tempfile::tempdir().unwrap();
2999 let log = Log::open_with(dir.path(), 300).unwrap();
3000 for i in 1..=20u64 {
3001 log.append(Kind::Pack, &meta("a/b"), &[i as u8; 100]).unwrap();
3002 }
3003 assert!(log.segments.lock().unwrap().len() > 3);
3004 assert_eq!(log.read(1).unwrap().blob[0], 1);
3005 let first = log.truncate_before(10).unwrap();
3006 assert!(first <= 10 && first > 1, "first={first}");
3007 assert!(matches!(log.read(1), Err(Error::NotRetained { .. })));
3008 assert_eq!(log.read(10).unwrap().blob[0], 10);
3009 assert_eq!(log.read(20).unwrap().blob[0], 20);
3010 drop(log);
3011 let log = Log::open_with(dir.path(), 300).unwrap();
3012 assert_eq!(log.first_seq(), first);
3013 assert_eq!(log.head_seq(), 20);
3014 assert_eq!(log.append(Kind::Pack, &meta("a/b"), &[21u8; 10]).unwrap(), 21);
3015 }
3016
3017 #[test]
3018 fn append_at_refuses_out_of_order() {
3019 let dir = tempfile::tempdir().unwrap();
3020 let log = Log::open(dir.path()).unwrap();
3021 let m = meta("a/b").to_bytes();
3022 assert_eq!(log.append_at(1, 1, &m, b"x").unwrap(), 1);
3023 assert!(matches!(log.append_at(3, 1, &m, b"x"), Err(Error::OutOfOrder { got: 3, expected: 2 })));
3024 assert!(matches!(log.append_at(1, 1, &m, b"x"), Err(Error::OutOfOrder { .. })));
3025 assert_eq!(log.append_at(2, 1, &m, b"x").unwrap(), 2);
3026 }
3027
3028 #[test]
3029 fn streaming_append_commits_and_aborts() {
3030 let dir = tempfile::tempdir().unwrap();
3031 let log = Log::open(dir.path()).unwrap();
3032 let m = meta("a/b").to_bytes();
3033 {
3034 let mut p = log.begin(Some(1), 1, &m, 6).unwrap();
3035 p.write(b"abc").unwrap();
3036 p.write(b"def").unwrap();
3037 assert_eq!(p.commit().unwrap(), 1);
3038 }
3039 {
3040 let mut p = log.begin(None, 1, &m, 6).unwrap();
3041 p.write(b"abc").unwrap();
3042 p.abort().unwrap();
3043 }
3044 assert_eq!(log.head_seq(), 1);
3045 {
3046 let mut p = log.begin(None, 1, &m, 3).unwrap();
3047 p.write(b"xy").unwrap();
3048 assert!(p.commit().is_err(), "short blob is refused");
3049 }
3050 assert_eq!(log.head_seq(), 1);
3051 assert_eq!(log.append_at(2, 1, &m, b"ok").unwrap(), 2);
3052 drop(log);
3053 let log = Log::open(dir.path()).unwrap();
3054 assert_eq!(log.head_seq(), 2);
3055 assert_eq!(log.read(1).unwrap().blob, b"abcdef");
3056 assert_eq!(log.read(2).unwrap().blob, b"ok");
3057 let l = log.locate_blob(1).unwrap();
3058 assert_eq!(l.blob_len, 6);
3059 let mut f = File::open(&l.path).unwrap();
3060 f.seek(SeekFrom::Start(l.blob_offset)).unwrap();
3061 let mut b = vec![0u8; 6];
3062 f.read_exact(&mut b).unwrap();
3063 assert_eq!(b, b"abcdef");
3064 }
3065
3066 #[test]
3072 fn a_streaming_append_dropped_half_way_leaves_nothing_and_the_next_append_reads_back() {
3073 let dir = tempfile::tempdir().unwrap();
3074 let log = Log::open(dir.path()).unwrap();
3075 let m = meta("a/b").to_bytes();
3076 assert_eq!(log.append_at(1, 1, &m, b"first").unwrap(), 1);
3077 let before = log.bytes();
3078 {
3079 let mut p = log.begin(Some(2), 1, &m, 1 << 20).unwrap();
3080 p.write(&[7u8; 4096]).unwrap();
3081 }
3083 assert_eq!(log.head_seq(), 1, "the dropped frame is not in the log");
3084 let seg = log.locate_blob(1).unwrap().path;
3085 assert_eq!(std::fs::metadata(&seg).unwrap().len(), before, "and not in the segment file either");
3086 assert_eq!(log.append_at(2, 1, &m, b"second").unwrap(), 2);
3087 assert_eq!(log.read(2).unwrap().blob, b"second", "the next append reads back in this process");
3088 drop(log);
3089 let log = Log::open(dir.path()).unwrap();
3090 assert_eq!(log.head_seq(), 2);
3091 assert_eq!(log.read(1).unwrap().blob, b"first");
3092 assert_eq!(log.read(2).unwrap().blob, b"second", "and after a restart");
3093 }
3094
3095 #[test]
3101 fn read_pieces_streams_the_blob_and_withholds_the_last_piece_of_a_corrupt_frame() {
3102 let dir = tempfile::tempdir().unwrap();
3103 let log = Log::open(dir.path()).unwrap();
3104 let blob: Vec<u8> = (0..10_000u32).map(|i| (i * 7 % 251) as u8).collect();
3105 let s = log.append(Kind::Pack, &meta("a/b"), &blob).unwrap();
3106 let empty = log.append(Kind::Refs, &meta("a/b"), b"").unwrap();
3107 let rot = log.append(Kind::Pack, &meta("a/c"), &blob).unwrap();
3108 let mut got = Vec::new();
3109 let mut lasts = 0;
3110 let mut want = Crc32c::new();
3112 want.update(&meta("a/b").to_bytes());
3113 want.update(&blob);
3114 let want = want.finish();
3115 let at = log
3116 .read_pieces::<Error>(s, 3000, |l, p, last| {
3117 assert_eq!(l.seq, s);
3118 assert!(p.len() <= 3000);
3119 assert_eq!(l.payload_crc, last.then_some(want), "the crc is handed over with the last piece and only then");
3120 got.extend_from_slice(p);
3121 lasts += usize::from(last);
3122 Ok(())
3123 })
3124 .unwrap();
3125 assert_eq!((got, lasts, at.blob_len), (blob.clone(), 1, blob.len() as u64));
3126 assert_eq!(at.payload_crc, Some(want));
3127 let mut tail = Vec::new();
3130 let at = log
3131 .read_pieces_from::<Error>(s, 4321, 3000, |l, p, last| {
3132 assert!(p.len() <= 3000);
3133 assert_eq!(l.payload_crc, last.then_some(want));
3134 tail.extend_from_slice(p);
3135 Ok(())
3136 })
3137 .unwrap();
3138 assert_eq!((tail.as_slice(), at.payload_crc), (&blob[4321..], Some(want)));
3139 let mut end = Vec::new();
3140 log.read_pieces_from::<Error>(s, blob.len() as u64, 3000, |l, p, last| {
3141 end.push((p.len(), last, l.payload_crc));
3142 Ok(())
3143 })
3144 .unwrap();
3145 assert_eq!(end, vec![(0, true, Some(want))]);
3146 assert!(log.read_pieces_from::<Error>(s, blob.len() as u64 + 1, 3000, |_, _, _| Ok(())).is_err());
3147 let mut pieces = Vec::new();
3148 log.read_pieces::<Error>(empty, 3000, |_, p, last| {
3149 pieces.push((p.len(), last));
3150 Ok(())
3151 })
3152 .unwrap();
3153 assert_eq!(pieces, vec![(0, true)]);
3154 let l = log.locate_blob(rot).unwrap();
3156 let mut f = OpenOptions::new().read(true).write(true).open(&l.path).unwrap();
3157 f.seek(SeekFrom::Start(l.blob_offset + 5000)).unwrap();
3158 let mut b = [0u8; 1];
3159 f.read_exact(&mut b).unwrap();
3160 f.seek(SeekFrom::Start(l.blob_offset + 5000)).unwrap();
3161 f.write_all(&[b[0] ^ 0xFF]).unwrap();
3162 drop(f);
3163 let mut seen = 0usize;
3164 let mut last_seen = false;
3165 let r = log.read_pieces::<Error>(rot, 3000, |_, p, last| {
3166 seen += p.len();
3167 last_seen |= last;
3168 Ok(())
3169 });
3170 assert!(matches!(r, Err(Error::Corrupt { what: "payload crc", .. })), "{r:?}");
3171 assert!(!last_seen, "the last piece of a corrupt frame is never handed over");
3172 assert!(seen < blob.len(), "{seen} of {} bytes went out before the refusal", blob.len());
3173 assert!(matches!(log.read(rot), Err(Error::Corrupt { .. })), "read refuses the same frame");
3174 log.erase(rot + 1, &["a/b".to_string()]).unwrap();
3176 let mut erased = Vec::new();
3177 log.read_pieces::<Error>(s, 3000, |l, p, last| {
3178 erased.push((l.kind, p.len(), last));
3179 Ok(())
3180 })
3181 .unwrap();
3182 assert_eq!(erased, vec![(Kind::Redacted as u16, 0, true)]);
3183 }
3184
3185 #[test]
3186 fn unknown_kind_is_stored_and_read_back() {
3187 let dir = tempfile::tempdir().unwrap();
3188 let log = Log::open(dir.path()).unwrap();
3189 log.append_raw(999, b"{}", b"future").unwrap();
3190 let e = log.read(1).unwrap();
3191 assert_eq!(e.kind(), None);
3192 assert_eq!(e.blob, b"future");
3193 }
3194
3195 #[test]
3196 fn reset_starts_over_at_one_and_refuses_a_box_with_history() {
3197 let dir = tempfile::tempdir().unwrap();
3198 let log = Log::open(dir.path()).unwrap();
3199 log.append(Kind::Kek, &Meta::Kek {}, &[7u8; 32]).unwrap();
3200 assert!(log.reset(0).is_err());
3202 assert_eq!(log.head_seq(), 1);
3203 log.reset(1).unwrap();
3204 assert_eq!(log.head_seq(), 0);
3205 assert_eq!(log.first_seq(), 1);
3206 log.append_at(1, Kind::Kek as u16, &Meta::Kek {}.to_bytes(), &[9u8; 32]).unwrap();
3208 drop(log);
3209 let log = Log::open(dir.path()).unwrap();
3210 assert_eq!(log.head_seq(), 1);
3211 assert_eq!(log.read(1).unwrap().blob, vec![9u8; 32]);
3212 }
3213
3214 #[test]
3215 fn kek_append_and_file_kinds_round_trip() {
3216 let dir = tempfile::tempdir().unwrap();
3217 let log = Log::open(dir.path()).unwrap();
3218 let metas = [
3219 (Kind::Kek, Meta::Kek {}),
3220 (Kind::Append, Meta::Append { log: "attestations.jsonl".into() }),
3221 (Kind::File, Meta::File { path: "replication.json".into(), deleted: false }),
3222 ];
3223 for (k, m) in &metas {
3224 log.append(*k, m, b"x").unwrap();
3225 }
3226 for (i, (k, m)) in metas.iter().enumerate() {
3227 let e = log.read(i as u64 + 1).unwrap();
3228 assert_eq!(e.kind(), Some(*k));
3229 assert_eq!(&e.meta().unwrap(), m);
3230 }
3231 }
3232
3233 #[test]
3234 fn a_follower_log_refuses_originations_and_takes_the_primarys_numbers() {
3235 let dir = tempfile::tempdir().unwrap();
3236 let log = Log::open(dir.path()).unwrap();
3237 log.set_follower(true);
3238 assert!(matches!(log.append(Kind::Pack, &meta("a/b"), b"x"), Err(Error::Follower)));
3239 assert!(matches!(log.begin(None, Kind::Pack as u16, b"{}", 1), Err(Error::Follower)));
3240 log.append_at(1, Kind::Pack as u16, &meta("a/b").to_bytes(), b"x").unwrap();
3241 assert_eq!(log.head_seq(), 1);
3242 log.set_follower(false);
3243 assert_eq!(log.append(Kind::Pack, &meta("a/b"), b"y").unwrap(), 2);
3244 }
3245
3246 #[test]
3247 fn cache_and_per_box_paths_are_judged_by_their_place_in_the_serialdat_tree() {
3248 for p in ["gc/x", ".serialdat/gc/x", "serialdat/quarantine/p.pack", ".serialdat/meta/identity.json", "repos/a/b.git/objects.exploded/1", ".korp-disktest/probe", "webhooks.json.tmp"] {
3249 assert!(is_cache_path(p) && never_framed(p), "{p} is cache");
3250 }
3251 for p in ["crash/panic-1", ".serialdat/log/console.txt", "log/audit-chain.local.jsonl", ".serialdat/log/audit-chain.local.jsonl", "twin.key"] {
3252 assert!(!is_cache_path(p) && is_per_box_path(p) && never_framed(p), "{p} is per box");
3253 }
3254 for p in ["attestations.jsonl", "replication.json", "repos/a/b.git/pack", "control.redb"] {
3255 assert!(!never_framed(p), "{p} is a transaction's path");
3256 }
3257 }
3258
3259 #[test]
3268 fn the_audit_trail_is_framed_and_the_twins_own_trail_is_not() {
3269 for p in ["log/audit-chain.jsonl", ".serialdat/log/audit-chain.jsonl", "serialdat/log/audit-chain.jsonl", "./log/audit-chain.jsonl"] {
3270 assert!(!is_per_box_path(p), "{p} is the audit trail and travels with the log");
3271 assert!(!never_framed(p), "{p} must be framable, or a restore loses the audit trail");
3272 }
3273 assert!(FRAMED_FILES.contains(&AUDIT_REL), "a snapshot must carry the audit trail");
3274 assert!(is_per_box_path(AUDIT_LOCAL_REL) && never_framed(AUDIT_LOCAL_REL));
3276 assert!(!FRAMED_FILES.contains(&AUDIT_LOCAL_REL));
3277 }
3278
3279 #[test]
3292 fn the_hub_declaration_travels_and_the_machines_own_identity_never_does() {
3293 for p in [HUB_CONF_REL, ".serialdat/wg/hub.conf", "./wg/hub.conf"] {
3294 assert!(!never_framed(p), "{p} must travel: a restored main without it is unreachable");
3295 }
3296 assert!(FRAMED_FILES.contains(&HUB_CONF_REL), "a snapshot must carry the hub declaration");
3297 for p in [WG_IDENTITY_KEY_REL, WG_IDENTITY_PUB_REL, HUB_CONF_FRAMED_REL] {
3298 assert!(is_per_box_path(p) && never_framed(p), "{p} is this machine's own and never travels");
3299 assert!(!FRAMED_FILES.contains(&p), "{p} must not be in a snapshot");
3300 }
3301 }
3302
3303 #[test]
3308 #[cfg(target_os = "linux")]
3309 fn a_released_log_holds_nothing_under_its_dir_refuses_writes_and_resumes_after_reacquire() {
3310 let root = tempfile::tempdir().unwrap();
3311 let dir = root.path().join("txlog");
3312 let log = Log::open(&dir).unwrap();
3313 let holds = |dir: &Path| -> Vec<PathBuf> {
3314 fs::read_dir("/proc/self/fd").unwrap().filter_map(|e| fs::read_link(e.ok()?.path()).ok()).filter(|p| p.starts_with(dir)).collect()
3315 };
3316 log.append(Kind::Kek, &Meta::Kek {}, b"one").unwrap();
3317 log.append(Kind::Kek, &Meta::Kek {}, b"two").unwrap();
3318 assert!(!holds(&dir).is_empty(), "the writer holds the open segment");
3319 log.release().unwrap();
3320 log.release().unwrap();
3321 assert!(log.is_released());
3322 assert_eq!(holds(&dir), Vec::<PathBuf>::new(), "released: nothing under the dir");
3323 assert!(matches!(log.append(Kind::Kek, &Meta::Kek {}, b"no"), Err(Error::Released(_))));
3324 assert!(matches!(log.begin(None, 1, b"", 1), Err(Error::Released(_))));
3325 assert_eq!(log.read(2).unwrap().blob, b"two", "readers open per call");
3326 log.reacquire().unwrap();
3327 log.reacquire().unwrap();
3328 assert_eq!(log.append(Kind::Kek, &Meta::Kek {}, b"three").unwrap(), 3);
3329 drop(log);
3330 let back = Log::open(&dir).unwrap();
3331 assert_eq!(back.head_seq(), 3);
3332 assert_eq!(back.read(3).unwrap().blob, b"three");
3333 }
3334
3335 #[test]
3337 fn reacquire_refuses_a_segment_that_changed_length() {
3338 let root = tempfile::tempdir().unwrap();
3339 let dir = root.path().join("txlog");
3340 let log = Log::open(&dir).unwrap();
3341 log.append(Kind::Kek, &Meta::Kek {}, b"one").unwrap();
3342 log.release().unwrap();
3343 OpenOptions::new().append(true).open(seg_path(&dir, 1)).unwrap().write_all(b"junk").unwrap();
3344 assert!(matches!(log.reacquire(), Err(Error::Corrupt { .. })));
3345 assert!(log.is_released(), "still refusing writes");
3346 }
3347
3348 #[test]
3349 fn the_sink_frames_writes_under_a_registered_root_and_nothing_elsewhere() {
3350 let root = tempfile::tempdir().unwrap();
3351 let other = tempfile::tempdir().unwrap();
3352 let log = Arc::new(Log::open(root.path().join("txlog")).unwrap());
3353 sink::register(root.path(), &log);
3354 drop(sink::append_line(&root.path().join("attestations.jsonl"), "{\"a\":1}").unwrap());
3355 drop(sink::replace_file(&root.path().join("webhooks.json"), b"[]", false).unwrap());
3356 drop(sink::append_line(&other.path().join("attestations.jsonl"), "{}").unwrap());
3357 assert_eq!(log.head_seq(), 2);
3358 assert_eq!(log.read(1).unwrap().meta().unwrap(), Meta::Append { log: "attestations.jsonl".into() });
3359 assert_eq!(log.read(2).unwrap().meta().unwrap(), Meta::File { path: "webhooks.json".into(), deleted: false });
3360 drop(log);
3361 drop(sink::append_line(&root.path().join("attestations.jsonl"), "{}").unwrap());
3363 }
3364
3365 #[test]
3366 fn restart_after_leaves_an_empty_log_whose_next_number_follows_the_snapshot_and_survives_a_reopen() {
3367 let dir = tempfile::tempdir().unwrap();
3368 {
3369 let log = Log::open(dir.path()).unwrap();
3370 log.append(Kind::Pack, &meta("a/b"), b"x").unwrap();
3371 log.restart_after(41).unwrap();
3372 assert_eq!((log.first_seq(), log.head_seq()), (42, 41));
3373 assert!(matches!(log.append_at(43, Kind::Pack as u16, b"{}", b""), Err(Error::OutOfOrder { .. })));
3374 assert_eq!(log.append_at(42, Kind::Pack as u16, &meta("a/b").to_bytes(), b"y").unwrap(), 42);
3375 }
3376 let log = Log::open(dir.path()).unwrap();
3377 assert_eq!((log.first_seq(), log.head_seq()), (42, 42));
3378 assert_eq!(log.read(42).unwrap().blob, b"y");
3379 assert!(matches!(log.read(1), Err(Error::NotRetained { .. })));
3380 }
3381
3382 #[test]
3383 fn a_freeze_waits_for_a_mutation_in_flight_and_holds_new_ones_until_it_ends() {
3384 use std::sync::atomic::AtomicU32;
3385 let dir = tempfile::tempdir().unwrap();
3386 let log = Arc::new(Log::open(dir.path()).unwrap());
3387 let stage = Arc::new(AtomicU32::new(0));
3388 let (l, s) = (Arc::clone(&log), Arc::clone(&stage));
3390 let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();
3391 let (in_tx, in_rx) = std::sync::mpsc::channel::<()>();
3392 let writer = std::thread::spawn(move || {
3393 let g = l.mutation();
3394 l.append(Kind::Pack, &meta("a/b"), b"1").unwrap();
3395 in_tx.send(()).unwrap();
3396 go_rx.recv().unwrap();
3397 s.store(1, Ordering::SeqCst); drop(g);
3399 });
3400 in_rx.recv().unwrap();
3401 let (l, s) = (Arc::clone(&log), Arc::clone(&stage));
3402 let (frozen_tx, frozen_rx) = std::sync::mpsc::channel::<()>();
3403 let freezer = std::thread::spawn(move || {
3404 let f = l.freeze();
3405 frozen_tx.send(()).unwrap();
3406 assert_eq!(s.load(Ordering::SeqCst), 1, "freeze returned before the mutation in flight ended");
3408 let head = l.head_seq();
3409 std::thread::sleep(std::time::Duration::from_millis(50));
3410 assert_eq!(l.head_seq(), head, "no mutation started while frozen");
3411 s.store(2, Ordering::SeqCst);
3412 drop(f);
3413 });
3414 std::thread::sleep(std::time::Duration::from_millis(30));
3415 go_tx.send(()).unwrap();
3416 writer.join().unwrap();
3417 frozen_rx.recv().unwrap();
3419 let g = log.mutation();
3420 assert_eq!(stage.load(Ordering::SeqCst), 2, "a mutation started while the log was frozen");
3421 log.append(Kind::Pack, &meta("a/b"), b"2").unwrap();
3422 drop(g);
3423 freezer.join().unwrap();
3424 let outer = log.mutation();
3426 let inner = log.mutation();
3427 drop(inner);
3428 drop(outer);
3429 let f = log.freeze();
3430 let own = log.mutation();
3431 drop(own);
3432 drop(f);
3433 }
3434}
3435
3436#[cfg(test)]
3437mod spool_tests {
3438 use super::*;
3442
3443 #[test]
3463 fn the_lfs_spool_directory_is_part_of_the_layout_and_does_not_refuse_the_log() {
3464 let dir = tempfile::tempdir().expect("temp dir");
3465 let log = Log::open(dir.path()).expect("a fresh log opens");
3466 drop(log);
3467 std::fs::create_dir_all(dir.path().join(SPOOL_DIR)).expect("the spool dir");
3468 Log::open(dir.path()).expect("a log whose spool directory exists still opens");
3469 }
3470
3471 #[test]
3475 fn a_stray_entry_is_still_refused_by_name() {
3476 let dir = tempfile::tempdir().expect("temp dir");
3477 Log::open(dir.path()).expect("a fresh log opens");
3478 std::fs::write(dir.path().join("stray"), b"x").expect("write");
3479 let err = Log::open(dir.path()).expect_err("a stray file is not part of the layout");
3480 assert!(matches!(err, Error::Unexpected(_)), "{err}");
3481 }
3482}