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, .. }
319 | Meta::Refs { repo, .. }
320 | Meta::Lfs { repo, .. }
321 | Meta::RepoStore { repo, .. } => Some(repo),
322 _ => None,
323 }
324 }
325}
326
327#[derive(Debug, Clone, Default, PartialEq, Eq)]
329pub struct Erased {
330 pub frames: u64,
332 pub bytes: u64,
334}
335
336impl Meta {
337 pub fn to_bytes(&self) -> Vec<u8> {
339 serde_json::to_vec(self).expect("Meta serializes")
340 }
341
342 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
344 serde_json::from_slice(bytes).map_err(|e| Error::BadMeta(e.to_string()))
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct Entry {
351 pub seq: u64,
353 pub kind: u16,
355 pub meta: Vec<u8>,
357 pub blob: Vec<u8>,
359}
360
361impl Entry {
362 pub fn kind(&self) -> Option<Kind> {
364 Kind::from_u16(self.kind)
365 }
366 pub fn meta(&self) -> Result<Meta> {
368 Meta::from_bytes(&self.meta)
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Eq)]
375pub struct Located {
376 pub seq: u64,
378 pub kind: u16,
380 pub meta: Vec<u8>,
382 pub path: PathBuf,
384 pub blob_offset: u64,
386 pub blob_len: u64,
388 pub payload_crc: Option<u32>,
397}
398
399#[derive(Debug, thiserror::Error)]
401pub enum Error {
402 #[error("txlog: {0}")]
404 Io(#[from] io::Error),
405 #[error("txlog: bad meta: {0}")]
407 BadMeta(String),
408 #[error("txlog: seq {seq} not retained (first retained is {first})")]
410 NotRetained {
411 seq: u64,
413 first: u64,
415 },
416 #[error("txlog: seq {seq} is beyond head {head}")]
418 BeyondHead {
419 seq: u64,
421 head: u64,
423 },
424 #[error("txlog: corrupt frame at {path}:{offset}: {what}")]
426 Corrupt {
427 path: PathBuf,
429 offset: u64,
431 what: &'static str,
433 },
434 #[error("txlog: released — the files under {0} are given up while the data set is grown; writes resume after the remount")]
437 Released(PathBuf),
438 #[error("txlog: append out of order: got {got}, expected {expected}")]
440 OutOfOrder {
441 got: u64,
443 expected: u64,
445 },
446 #[error("txlog: this box is a twin (or restoring); it originates no transactions")]
450 Follower,
451 #[error("txlog: unexpected file {0}")]
453 Unexpected(PathBuf),
454 #[error("txlog: poisoned under {dir} — {cause}; every write refuses until the log is reopened from its last durable frame")]
462 Poisoned {
463 dir: PathBuf,
465 cause: String,
467 },
468}
469
470pub type Result<T> = std::result::Result<T, Error>;
472
473const CRC32C_POLY: u32 = 0x82F6_3B78;
476
477const fn crc32c_table() -> [u32; 256] {
478 let mut table = [0u32; 256];
479 let mut i = 0;
480 while i < 256 {
481 let mut c = i as u32;
482 let mut k = 0;
483 while k < 8 {
484 c = if c & 1 != 0 {
485 CRC32C_POLY ^ (c >> 1)
486 } else {
487 c >> 1
488 };
489 k += 1;
490 }
491 table[i] = c;
492 i += 1;
493 }
494 table
495}
496
497static CRC32C_TABLE: [u32; 256] = crc32c_table();
498
499#[derive(Debug, Clone, Copy)]
501pub struct Crc32c(u32);
502
503impl Default for Crc32c {
504 fn default() -> Self {
505 Self::new()
506 }
507}
508
509impl Crc32c {
510 pub const fn new() -> Self {
512 Crc32c(0xFFFF_FFFF)
513 }
514 pub fn update(&mut self, bytes: &[u8]) {
516 let mut c = self.0;
517 for &b in bytes {
518 c = CRC32C_TABLE[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
519 }
520 self.0 = c;
521 }
522 pub fn finish(self) -> u32 {
524 self.0 ^ 0xFFFF_FFFF
525 }
526 pub fn state(&self) -> u32 {
530 self.0
531 }
532 pub const fn resume(state: u32) -> Self {
534 Crc32c(state)
535 }
536}
537
538pub fn crc32c(bytes: &[u8]) -> u32 {
540 let mut c = Crc32c::new();
541 c.update(bytes);
542 c.finish()
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
548struct Header {
549 seq: u64,
550 kind: u16,
551 meta_len: u32,
552 blob_len: u64,
553}
554
555impl Header {
556 fn encode(&self) -> [u8; HEADER_LEN] {
557 let mut h = [0u8; HEADER_LEN];
558 h[0..4].copy_from_slice(&MAGIC);
559 h[4..12].copy_from_slice(&self.seq.to_le_bytes());
560 h[12..14].copy_from_slice(&self.kind.to_le_bytes());
561 h[14..16].copy_from_slice(&0u16.to_le_bytes());
562 h[16..20].copy_from_slice(&self.meta_len.to_le_bytes());
563 h[20..28].copy_from_slice(&self.blob_len.to_le_bytes());
564 let crc = crc32c(&h[0..28]);
565 h[28..32].copy_from_slice(&crc.to_le_bytes());
566 h
567 }
568
569 fn decode(h: &[u8]) -> Option<Self> {
570 if h.len() < HEADER_LEN || h[0..4] != MAGIC {
571 return None;
572 }
573 let crc = u32::from_le_bytes(h[28..32].try_into().ok()?);
574 if crc32c(&h[0..28]) != crc {
575 return None;
576 }
577 Some(Header {
578 seq: u64::from_le_bytes(h[4..12].try_into().ok()?),
579 kind: u16::from_le_bytes(h[12..14].try_into().ok()?),
580 meta_len: u32::from_le_bytes(h[16..20].try_into().ok()?),
581 blob_len: u64::from_le_bytes(h[20..28].try_into().ok()?),
582 })
583 }
584
585 fn frame_len(&self) -> u64 {
586 HEADER_LEN as u64 + self.meta_len as u64 + self.blob_len + TRAILER_LEN as u64
587 }
588}
589
590fn seg_path(dir: &Path, first: u64) -> PathBuf {
593 dir.join(format!("{SEG_PREFIX}{first:020}.{SEG_EXT}"))
594}
595
596fn idx_path(dir: &Path, first: u64) -> PathBuf {
597 dir.join(format!("{SEG_PREFIX}{first:020}.{IDX_EXT}"))
598}
599
600fn parse_seg_name(name: &str) -> Option<(u64, bool)> {
601 let rest = name.strip_prefix(SEG_PREFIX)?;
602 let (num, ext) = rest.split_once('.')?;
603 let first: u64 = num.parse().ok()?;
604 match ext {
605 SEG_EXT => Some((first, true)),
606 IDX_EXT => Some((first, false)),
607 _ => None,
608 }
609}
610
611fn sync_dir(dir: &Path) -> io::Result<()> {
612 File::open(dir)?.sync_all()
613}
614
615fn zero_range(file: &mut File, at: u64, len: u64) -> io::Result<()> {
618 if len == 0 {
619 return Ok(());
620 }
621 #[cfg(target_os = "linux")]
622 {
623 use rustix::fs::{fallocate, FallocateFlags};
624 if fallocate(
625 &*file,
626 FallocateFlags::PUNCH_HOLE | FallocateFlags::KEEP_SIZE,
627 at,
628 len,
629 )
630 .is_ok()
631 {
632 return Ok(());
633 }
634 }
635 let zeros = vec![0u8; (1usize << 20).min(len as usize)];
636 file.seek(SeekFrom::Start(at))?;
637 let mut left = len;
638 while left > 0 {
639 let n = left.min(zeros.len() as u64) as usize;
640 file.write_all(&zeros[..n])?;
641 left -= n as u64;
642 }
643 Ok(())
644}
645
646#[derive(Debug)]
648struct Segment {
649 first: u64,
650 offsets: Vec<u64>,
652 len: u64,
654}
655
656impl Segment {
657 fn last_seq(&self) -> Option<u64> {
658 if self.offsets.is_empty() {
659 None
660 } else {
661 Some(self.first + self.offsets.len() as u64 - 1)
662 }
663 }
664}
665
666fn scan_frames(
669 path: &Path,
670 file: &mut File,
671 first: u64,
672 offsets: &mut Vec<u64>,
673 from: u64,
674) -> Result<(u64, bool)> {
675 let total = file.metadata()?.len();
676 let mut pos = from;
677 let mut hdr = [0u8; HEADER_LEN];
678 let mut buf = Vec::new();
679 while pos < total {
680 if total - pos < HEADER_LEN as u64 {
681 return Ok((pos, true));
682 }
683 file.seek(SeekFrom::Start(pos))?;
684 file.read_exact(&mut hdr)?;
685 let Some(h) = Header::decode(&hdr) else {
686 return Ok((pos, true));
687 };
688 let expect = first + offsets.len() as u64;
689 if h.seq != expect {
690 return Ok((pos, true));
691 }
692 let payload = h.meta_len as u64 + h.blob_len;
693 if total - pos < h.frame_len() {
694 return Ok((pos, true));
695 }
696 if h.kind == Kind::Redacted as u16 {
699 offsets.push(pos);
700 pos += h.frame_len();
701 continue;
702 }
703 let mut crc = Crc32c::new();
705 let mut remaining = payload;
706 buf.resize(1 << 20, 0);
707 while remaining > 0 {
708 let n = remaining.min(buf.len() as u64) as usize;
709 file.read_exact(&mut buf[..n])?;
710 crc.update(&buf[..n]);
711 remaining -= n as u64;
712 }
713 let mut tr = [0u8; TRAILER_LEN];
714 file.read_exact(&mut tr)?;
715 if u32::from_le_bytes(tr) != crc.finish() {
716 return Ok((pos, true));
717 }
718 let _ = path;
719 offsets.push(pos);
720 pos += h.frame_len();
721 }
722 Ok((pos, false))
723}
724
725fn open_segment(dir: &Path, first: u64, repair: bool) -> Result<Segment> {
726 let path = seg_path(dir, first);
727 let mut file = OpenOptions::new().read(true).write(repair).open(&path)?;
728 let total = file.metadata()?.len();
729
730 let mut offsets: Vec<u64> = Vec::new();
732 let ipath = idx_path(dir, first);
733 if let Ok(bytes) = fs::read(&ipath) {
734 let n = bytes.len() / 8;
735 offsets.reserve(n);
736 for i in 0..n {
737 let off = u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().expect("8 bytes"));
738 offsets.push(off);
739 }
740 let monotone = offsets.windows(2).all(|w| w[0] < w[1]);
742 if !monotone || offsets.first().is_some_and(|&o| o != 0) {
743 offsets.clear();
744 }
745 while let Some(&last) = offsets.last() {
746 if last + HEADER_LEN as u64 > total {
747 offsets.pop();
748 continue;
749 }
750 file.seek(SeekFrom::Start(last))?;
751 let mut hdr = [0u8; HEADER_LEN];
752 file.read_exact(&mut hdr)?;
753 match Header::decode(&hdr) {
754 Some(h)
755 if h.seq == first + offsets.len() as u64 - 1
756 && last + h.frame_len() <= total =>
757 {
758 break;
759 }
760 _ => {
761 offsets.pop();
762 }
763 }
764 }
765 }
766
767 let trusted = offsets.len();
773 let resume = offsets.pop().unwrap_or(0);
774 let (valid_len, torn) = scan_frames(&path, &mut file, first, &mut offsets, resume)?;
775
776 if torn && repair {
777 tracing::warn!(
778 segment = %path.display(),
779 kept = valid_len,
780 discarded = total - valid_len,
781 "txlog: torn tail cut"
782 );
783 file.set_len(valid_len)?;
784 file.sync_all()?;
785 }
786 if (torn && repair) || offsets.len() != trusted {
787 if repair {
789 let mut bytes = Vec::with_capacity(offsets.len() * 8);
790 for off in &offsets {
791 bytes.extend_from_slice(&off.to_le_bytes());
792 }
793 fs::write(&ipath, bytes)?;
794 }
795 }
796 Ok(Segment {
797 first,
798 offsets,
799 len: valid_len,
800 })
801}
802
803struct Writer {
806 file: File,
808 idx: File,
810 seg: Segment,
814 disk_len: u64,
816 buf: Vec<u8>,
821 published: usize,
824 #[cfg(test)]
826 faults: WriterFaults,
827}
828
829#[cfg(test)]
831#[derive(Default)]
832struct WriterFaults {
833 fail_put_after: Option<usize>,
835 fail_cut: bool,
837}
838
839const STAGE_BYTES: usize = 1 << 20;
841const DIRECT_BYTES: usize = 64 << 10;
844
845impl Writer {
846 fn flush_buf(&mut self) -> io::Result<()> {
848 if !self.buf.is_empty() {
849 self.file.write_all(&self.buf)?;
850 self.disk_len += self.buf.len() as u64;
851 self.buf.clear();
852 }
853 Ok(())
854 }
855
856 fn put(&mut self, bytes: &[u8]) -> io::Result<()> {
858 #[cfg(test)]
859 if let Some(n) = self.faults.fail_put_after {
860 if n == 0 {
861 self.faults.fail_put_after = None;
862 return Err(io::Error::other("txlog test: injected write failure"));
863 }
864 self.faults.fail_put_after = Some(n - 1);
865 }
866 if bytes.len() >= DIRECT_BYTES {
867 self.flush_buf()?;
868 self.file.write_all(bytes)?;
869 self.disk_len += bytes.len() as u64;
870 return Ok(());
871 }
872 self.buf.extend_from_slice(bytes);
873 if self.buf.len() >= STAGE_BYTES {
874 self.flush_buf()?;
875 }
876 Ok(())
877 }
878
879 fn unpublished(&self) -> usize {
881 self.seg.offsets.len() - self.published
882 }
883
884 fn cut_to_committed(&mut self) -> io::Result<()> {
886 #[cfg(test)]
887 if std::mem::take(&mut self.faults.fail_cut) {
888 return Err(io::Error::other("txlog test: injected truncate failure"));
889 }
890 let keep = self.seg.len;
891 if keep >= self.disk_len {
892 self.buf.truncate((keep - self.disk_len) as usize);
894 return Ok(());
895 }
896 self.buf.clear();
899 self.file.set_len(keep)?;
900 self.file.seek(SeekFrom::Start(keep))?;
901 self.disk_len = keep;
902 self.file.sync_data()
903 }
904}
905
906pub const TEST_GROUP_ENV: &str = "GUNNAR_TXLOG_TEST_GROUP";
912
913#[cfg(feature = "test-hooks")]
916fn test_group_from_env() -> Option<usize> {
917 std::env::var(TEST_GROUP_ENV)
918 .ok()
919 .and_then(|v| v.trim().parse::<usize>().ok())
920 .filter(|n| *n > 1)
921}
922
923#[cfg(not(feature = "test-hooks"))]
924fn test_group_from_env() -> Option<usize> {
925 None
926}
927
928pub const TEST_GROUP_WAIT: std::time::Duration = std::time::Duration::from_millis(100);
931
932pub struct Log {
937 dir: PathBuf,
938 segments: Mutex<Vec<Segment>>,
940 writer: Mutex<Writer>,
941 head: AtomicU64,
942 first: AtomicU64,
943 segment_bytes: u64,
944 follower: AtomicBool,
946 gate: Arc<Gate>,
948 released: AtomicBool,
950 poisoned: Mutex<Option<String>>,
952 sync_state: Mutex<bool>,
960 synced: std::sync::Condvar,
962 #[cfg(test)]
964 fsync_gates: Mutex<std::collections::VecDeque<std::sync::mpsc::Receiver<()>>>,
965 #[cfg(test)]
967 fail_fsyncs: std::sync::atomic::AtomicU32,
968 test_group: Option<usize>,
972 frame_syncs: AtomicU64,
974 erase_syncs: AtomicU64,
976 erase_lock: Mutex<()>,
978 #[cfg(test)]
980 erase_gates: Mutex<std::collections::VecDeque<std::sync::mpsc::Receiver<()>>>,
981}
982
983#[derive(Default)]
989struct Gate {
990 state: Mutex<GateState>,
991 cv: std::sync::Condvar,
992}
993
994#[derive(Default)]
995struct GateState {
996 frozen_by: Option<std::thread::ThreadId>,
997 inflight: u64,
998}
999
1000thread_local! {
1001 static MUTATION_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
1003}
1004
1005#[must_use = "a mutation guard must be held until the store has taken the write"]
1007pub struct MutationGuard {
1008 gate: Option<Arc<Gate>>,
1009}
1010
1011impl Drop for MutationGuard {
1012 fn drop(&mut self) {
1013 MUTATION_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
1014 if let Some(gate) = self.gate.take() {
1015 let mut st = gate.state.lock().unwrap_or_else(|p| p.into_inner());
1016 st.inflight = st.inflight.saturating_sub(1);
1017 gate.cv.notify_all();
1018 }
1019 }
1020}
1021
1022pub struct FreezeGuard {
1024 gate: Arc<Gate>,
1025}
1026
1027impl Drop for FreezeGuard {
1028 fn drop(&mut self) {
1029 let mut st = self.gate.state.lock().unwrap_or_else(|p| p.into_inner());
1030 st.frozen_by = None;
1031 self.gate.cv.notify_all();
1032 }
1033}
1034
1035impl std::fmt::Debug for Log {
1036 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1037 f.debug_struct("Log")
1038 .field("dir", &self.dir)
1039 .field("first", &self.first_seq())
1040 .field("head", &self.head_seq())
1041 .finish()
1042 }
1043}
1044
1045pub const SPOOL_DIR: &str = "spool";
1060
1061impl Log {
1062 pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
1064 Self::open_with(dir, DEFAULT_SEGMENT_BYTES)
1065 }
1066
1067 pub fn open_with(dir: impl AsRef<Path>, segment_bytes: u64) -> Result<Self> {
1069 let dir = dir.as_ref().to_path_buf();
1070 fs::create_dir_all(&dir)?;
1071 let mut firsts: Vec<u64> = Vec::new();
1072 for entry in fs::read_dir(&dir)? {
1073 let entry = entry?;
1074 let name = entry.file_name();
1075 let name = name.to_string_lossy();
1076 match parse_seg_name(&name) {
1077 Some((first, true)) => firsts.push(first),
1078 Some((_, false)) => {}
1079 None if name == SPOOL_DIR && entry.file_type().is_ok_and(|t| t.is_dir()) => {}
1085 None => return Err(Error::Unexpected(entry.path())),
1086 }
1087 }
1088 firsts.sort_unstable();
1089 let mut segments = Vec::with_capacity(firsts.len().max(1));
1090 let n = firsts.len();
1091 for (i, first) in firsts.iter().enumerate() {
1092 let seg = open_segment(&dir, *first, true)?;
1093 if i + 1 < n && seg.offsets.is_empty() {
1096 return Err(Error::Corrupt {
1097 path: seg_path(&dir, *first),
1098 offset: 0,
1099 what: "closed segment holds no frame",
1100 });
1101 }
1102 if let Some(next) = firsts.get(i + 1) {
1104 match seg.last_seq() {
1105 Some(last) if last + 1 == *next => {}
1106 _ => {
1107 return Err(Error::Corrupt {
1108 path: seg_path(&dir, *first),
1109 offset: seg.len,
1110 what: "segment does not run up to the next one",
1111 })
1112 }
1113 }
1114 }
1115 segments.push(seg);
1116 }
1117 if segments.is_empty() {
1118 let first = 1;
1119 File::create(seg_path(&dir, first))?;
1120 File::create(idx_path(&dir, first))?;
1121 sync_dir(&dir)?;
1122 segments.push(Segment {
1123 first,
1124 offsets: Vec::new(),
1125 len: 0,
1126 });
1127 }
1128 let open = segments.last().expect("one segment");
1129 let head = open.last_seq().unwrap_or(open.first - 1);
1130 let first = segments[0].first;
1131 let mut file = OpenOptions::new()
1132 .append(true)
1133 .open(seg_path(&dir, open.first))?;
1134 file.seek(SeekFrom::Start(open.len))?;
1135 let idx = OpenOptions::new()
1136 .append(true)
1137 .open(idx_path(&dir, open.first))?;
1138 let seg = Segment {
1139 first: open.first,
1140 offsets: open.offsets.clone(),
1141 len: open.len,
1142 };
1143 let published = seg.offsets.len();
1144 Ok(Log {
1145 dir,
1146 segments: Mutex::new(segments),
1147 writer: Mutex::new(Writer {
1148 file,
1149 idx,
1150 disk_len: seg.len,
1151 seg,
1152 buf: Vec::new(),
1153 published,
1154 #[cfg(test)]
1155 faults: WriterFaults::default(),
1156 }),
1157 head: AtomicU64::new(head),
1158 first: AtomicU64::new(first),
1159 segment_bytes,
1160 follower: AtomicBool::new(false),
1161 gate: Arc::new(Gate::default()),
1162 released: AtomicBool::new(false),
1163 poisoned: Mutex::new(None),
1164 sync_state: Mutex::new(false),
1165 synced: std::sync::Condvar::new(),
1166 #[cfg(test)]
1167 fsync_gates: Mutex::new(Default::default()),
1168 #[cfg(test)]
1169 fail_fsyncs: std::sync::atomic::AtomicU32::new(0),
1170 test_group: test_group_from_env(),
1171 frame_syncs: AtomicU64::new(0),
1172 erase_syncs: AtomicU64::new(0),
1173 erase_lock: Mutex::new(()),
1174 #[cfg(test)]
1175 erase_gates: Mutex::new(Default::default()),
1176 })
1177 }
1178
1179 pub fn mutation(&self) -> MutationGuard {
1184 let depth = MUTATION_DEPTH.with(|d| {
1185 let v = d.get();
1186 d.set(v + 1);
1187 v
1188 });
1189 if depth > 0 {
1190 return MutationGuard { gate: None };
1191 }
1192 let me = std::thread::current().id();
1193 let mut st = self.gate.state.lock().unwrap_or_else(|p| p.into_inner());
1194 while matches!(st.frozen_by, Some(t) if t != me) {
1195 st = self.gate.cv.wait(st).unwrap_or_else(|p| p.into_inner());
1196 }
1197 st.inflight += 1;
1198 MutationGuard {
1199 gate: Some(Arc::clone(&self.gate)),
1200 }
1201 }
1202
1203 pub fn freeze(&self) -> FreezeGuard {
1207 debug_assert_eq!(
1208 MUTATION_DEPTH.with(|d| d.get()),
1209 0,
1210 "freeze from inside a mutation waits for itself"
1211 );
1212 let me = std::thread::current().id();
1213 let mut st = self.gate.state.lock().unwrap_or_else(|p| p.into_inner());
1214 while st.frozen_by.is_some_and(|t| t != me) {
1215 st = self.gate.cv.wait(st).unwrap_or_else(|p| p.into_inner());
1216 }
1217 st.frozen_by = Some(me);
1218 while st.inflight > 0 {
1219 st = self.gate.cv.wait(st).unwrap_or_else(|p| p.into_inner());
1220 }
1221 FreezeGuard {
1222 gate: Arc::clone(&self.gate),
1223 }
1224 }
1225
1226 pub fn dir(&self) -> &Path {
1228 &self.dir
1229 }
1230
1231 pub fn set_follower(&self, on: bool) {
1234 self.follower.store(on, Ordering::Release);
1235 }
1236
1237 pub fn is_follower(&self) -> bool {
1239 self.follower.load(Ordering::Acquire)
1240 }
1241
1242 pub fn group_hold(&self) -> Option<usize> {
1245 self.test_group
1246 }
1247
1248 pub fn head_seq(&self) -> u64 {
1250 self.head.load(Ordering::Acquire)
1251 }
1252
1253 pub fn first_seq(&self) -> u64 {
1255 self.first.load(Ordering::Acquire)
1256 }
1257
1258 pub fn append(&self, kind: Kind, meta: &Meta, blob: &[u8]) -> Result<u64> {
1261 if self.is_follower() {
1262 return Err(Error::Follower);
1263 }
1264 let meta = meta.to_bytes();
1265 self.append_raw(kind as u16, &meta, blob)
1266 }
1267
1268 pub fn append_at(&self, seq: u64, kind: u16, meta: &[u8], blob: &[u8]) -> Result<u64> {
1272 {
1273 let mut w = self.lock_writer()?;
1274 let expected = w.seg.first + w.seg.offsets.len() as u64;
1275 if seq != expected {
1276 return Err(Error::OutOfOrder { got: seq, expected });
1277 }
1278 self.stage_locked(&mut w, kind, meta, blob)?;
1279 }
1280 self.sync_through(seq)?;
1281 Ok(seq)
1282 }
1283
1284 pub fn append_raw(&self, kind: u16, meta: &[u8], blob: &[u8]) -> Result<u64> {
1292 let seq = {
1293 let mut w = self.lock_writer()?;
1294 self.stage_locked(&mut w, kind, meta, blob)?
1295 };
1296 self.sync_through(seq)?;
1297 Ok(seq)
1298 }
1299
1300 pub fn stage(&self, kind: Kind, meta: &Meta, blob: &[u8]) -> Result<u64> {
1305 if self.is_follower() {
1306 return Err(Error::Follower);
1307 }
1308 self.stage_raw(kind as u16, &meta.to_bytes(), blob)
1309 }
1310
1311 pub fn stage_raw(&self, kind: u16, meta: &[u8], blob: &[u8]) -> Result<u64> {
1316 let mut w = self.lock_writer()?;
1317 self.stage_locked(&mut w, kind, meta, blob)
1318 }
1319
1320 fn stage_locked(&self, w: &mut Writer, kind: u16, meta: &[u8], blob: &[u8]) -> Result<u64> {
1321 self.roll_if_full(w)?;
1322 let seq = w.seg.first + w.seg.offsets.len() as u64;
1323 let header = Header {
1324 seq,
1325 kind,
1326 meta_len: meta.len() as u32,
1327 blob_len: blob.len() as u64,
1328 };
1329 let h = header.encode();
1330 let mut crc = Crc32c::new();
1331 crc.update(meta);
1332 crc.update(blob);
1333 let tr = crc.finish().to_le_bytes();
1334 let put = (|| {
1335 w.put(&h)?;
1336 w.put(meta)?;
1337 w.put(blob)?;
1338 w.put(&tr)
1339 })();
1340 if let Err(e) = put {
1341 if let Err(cut) = w.cut_to_committed() {
1344 self.poison(format!("a half-written frame could not be cut off the segment ({cut}) after its write failed ({e})"));
1345 }
1346 return Err(e.into());
1347 }
1348 let off = w.seg.len;
1349 w.seg.offsets.push(off);
1350 w.seg.len += header.frame_len();
1351 Ok(seq)
1352 }
1353
1354 pub fn sync_through(&self, seq: u64) -> Result<()> {
1361 {
1362 let mut leading = self.sync_state.lock().unwrap_or_else(|p| p.into_inner());
1363 loop {
1364 if self.head_seq() >= seq {
1365 return Ok(());
1366 }
1367 if !*leading {
1368 *leading = true;
1369 break;
1370 }
1371 leading = self.synced.wait(leading).unwrap_or_else(|p| p.into_inner());
1372 }
1373 }
1374 struct Lead<'a>(&'a Log);
1377 impl Drop for Lead<'_> {
1378 fn drop(&mut self) {
1379 *self.0.sync_state.lock().unwrap_or_else(|p| p.into_inner()) = false;
1380 self.0.synced.notify_all();
1381 }
1382 }
1383 let _lead = Lead(self);
1384 if let Some(n) = self.test_group {
1385 self.hold_for_the_group(n);
1386 }
1387 let snap = (|| -> Result<(File, u64, usize)> {
1388 let mut w = self.lock_writer()?;
1389 w.flush_buf()?;
1390 Ok((w.file.try_clone()?, w.seg.first, w.seg.offsets.len()))
1391 })();
1392 let (file, first, upto) = match snap {
1393 Ok(s) => s,
1394 Err(_) if self.head_seq() >= seq => return Ok(()),
1396 Err(e) => return Err(e),
1397 };
1398 #[cfg(test)]
1399 {
1400 let gate = self
1401 .fsync_gates
1402 .lock()
1403 .unwrap_or_else(|p| p.into_inner())
1404 .pop_front();
1405 if let Some(gate) = gate {
1406 let _ = gate.recv();
1407 }
1408 }
1409 if let Err(e) = self.sync_frames(&file) {
1410 let mut w = self.writer.lock().expect("txlog writer");
1411 if w.seg.first == first {
1412 return Err(self.poison_and_cut(&mut w, e));
1413 }
1414 drop(w);
1417 if self.head_seq() < seq {
1418 return Err(self.poison_and_cut(&mut self.writer.lock().expect("txlog writer"), e));
1419 }
1420 return Ok(());
1421 }
1422 self.frame_syncs.fetch_add(1, Ordering::Relaxed);
1423 let mut w = match self.lock_writer() {
1424 Ok(w) => w,
1425 Err(_) if self.head_seq() >= seq => return Ok(()),
1426 Err(e) => return Err(e),
1427 };
1428 if w.seg.first == first && upto > w.published {
1430 self.publish_upto(&mut w, upto)?;
1431 }
1432 Ok(())
1433 }
1434
1435 fn hold_for_the_group(&self, n: usize) {
1440 let until = std::time::Instant::now() + TEST_GROUP_WAIT;
1441 loop {
1442 let staged = self.writer.lock().map(|w| w.unpublished()).unwrap_or(n);
1443 if staged >= n || std::time::Instant::now() >= until {
1444 return;
1445 }
1446 std::thread::sleep(std::time::Duration::from_micros(200));
1447 }
1448 }
1449
1450 fn sync_frames(&self, file: &File) -> io::Result<()> {
1453 #[cfg(test)]
1454 loop {
1455 let n = self.fail_fsyncs.load(Ordering::SeqCst);
1456 if n == 0 {
1457 break;
1458 }
1459 if self
1460 .fail_fsyncs
1461 .compare_exchange(n, n - 1, Ordering::SeqCst, Ordering::SeqCst)
1462 .is_ok()
1463 {
1464 return Err(io::Error::other("txlog test: injected fsync failure (EIO)"));
1465 }
1466 }
1467 file.sync_data()
1468 }
1469
1470 #[cfg(test)]
1472 fn fail_next_fsyncs(&self, n: u32) {
1473 self.fail_fsyncs.store(n, Ordering::SeqCst);
1474 }
1475
1476 #[cfg(test)]
1478 fn hold_next_fsync(&self, gate: std::sync::mpsc::Receiver<()>) {
1479 self.fsync_gates.lock().unwrap().push_back(gate);
1480 }
1481
1482 pub fn sync(&self) -> Result<u64> {
1488 let mut w = self.lock_writer()?;
1489 self.publish(&mut w)?;
1490 Ok(self.head_seq())
1491 }
1492
1493 pub fn unsynced(&self) -> u64 {
1495 self.writer.lock().expect("txlog writer").unpublished() as u64
1496 }
1497
1498 fn publish(&self, w: &mut Writer) -> Result<()> {
1500 w.flush_buf()?;
1501 let n = w.unpublished();
1502 if n == 0 {
1503 return Ok(());
1504 }
1505 if let Some(cause) = self.poisoned_by() {
1506 return Err(Error::Poisoned {
1507 dir: self.dir.clone(),
1508 cause,
1509 });
1510 }
1511 if let Err(e) = self.sync_frames(&w.file) {
1512 return Err(self.poison_and_cut(w, e));
1513 }
1514 self.frame_syncs.fetch_add(1, Ordering::Relaxed);
1515 self.publish_upto(w, w.seg.offsets.len())
1516 }
1517
1518 pub fn poisoned_by(&self) -> Option<String> {
1520 self.poisoned
1521 .lock()
1522 .unwrap_or_else(|p| p.into_inner())
1523 .clone()
1524 }
1525
1526 fn poison(&self, cause: String) {
1528 let mut p = self.poisoned.lock().unwrap_or_else(|p| p.into_inner());
1529 if p.is_none() {
1530 tracing::error!(
1531 target: crate::observe::TARGET_STORE,
1532 event = "txlog.poisoned",
1533 dir = %self.dir.display(),
1534 cause = %cause,
1535 "the transaction log is poisoned: every write refuses until it is reopened"
1536 );
1537 *p = Some(cause);
1538 }
1539 }
1540
1541 fn poison_and_cut(&self, w: &mut Writer, e: io::Error) -> Error {
1545 let mut cause = format!("a frame fsync failed ({e})");
1546 let keep = w.seg.offsets.get(w.published).copied().unwrap_or(w.seg.len);
1547 w.seg.offsets.truncate(w.published);
1548 w.seg.len = keep;
1549 if keep >= w.disk_len {
1550 w.buf.truncate((keep - w.disk_len) as usize);
1551 } else {
1552 w.buf.clear();
1553 let cut = (|| {
1554 w.file.set_len(keep)?;
1555 w.file.seek(SeekFrom::Start(keep))?;
1556 w.disk_len = keep;
1557 w.file.sync_data()
1558 })();
1559 if let Err(c) = cut {
1560 cause.push_str(&format!(
1561 ", and the frames it did not make durable could not be cut ({c})"
1562 ));
1563 }
1564 }
1565 self.poison(cause.clone());
1566 Error::Poisoned {
1567 dir: self.dir.clone(),
1568 cause,
1569 }
1570 }
1571
1572 pub fn frame_syncs(&self) -> u64 {
1576 self.frame_syncs.load(Ordering::Relaxed)
1577 }
1578
1579 fn publish_upto(&self, w: &mut Writer, upto: usize) -> Result<()> {
1581 if upto <= w.published {
1582 return Ok(());
1583 }
1584 let fresh: Vec<u64> = w.seg.offsets[w.published..upto].to_vec();
1585 let mut bytes = Vec::with_capacity(fresh.len() * 8);
1586 for off in &fresh {
1587 bytes.extend_from_slice(&off.to_le_bytes());
1588 }
1589 w.idx.write_all(&bytes)?;
1592 w.published = upto;
1593 let end = match w.seg.offsets.get(upto) {
1594 Some(&next) => next,
1595 None => w.seg.len,
1596 };
1597 {
1598 let mut segs = self.segments.lock().expect("txlog segments");
1599 let open = segs.last_mut().expect("open segment");
1600 debug_assert_eq!(
1601 open.first, w.seg.first,
1602 "the published open segment is the writer's"
1603 );
1604 open.offsets.extend_from_slice(&fresh);
1605 open.len = end;
1606 }
1607 let last = w.seg.first + upto as u64 - 1;
1608 self.head.store(last, Ordering::Release);
1609 Ok(())
1610 }
1611
1612 pub fn begin(
1617 &self,
1618 seq: Option<u64>,
1619 kind: u16,
1620 meta: &[u8],
1621 blob_len: u64,
1622 ) -> Result<Pending<'_>> {
1623 if seq.is_none() && self.is_follower() {
1624 return Err(Error::Follower);
1625 }
1626 let mut w = self.lock_writer()?;
1627 self.roll_if_full(&mut w)?;
1628 let next = w.seg.first + w.seg.offsets.len() as u64;
1629 if let Some(seq) = seq {
1630 if seq != next {
1631 return Err(Error::OutOfOrder {
1632 got: seq,
1633 expected: next,
1634 });
1635 }
1636 }
1637 let header = Header {
1638 seq: next,
1639 kind,
1640 meta_len: meta.len() as u32,
1641 blob_len,
1642 };
1643 let h = header.encode();
1644 w.put(&h)?;
1645 w.put(meta)?;
1646 let mut crc = Crc32c::new();
1647 crc.update(meta);
1648 Ok(Pending {
1649 log: self,
1650 w,
1651 header,
1652 crc,
1653 written: 0,
1654 finished: false,
1655 })
1656 }
1657
1658 fn lock_writer(&self) -> Result<std::sync::MutexGuard<'_, Writer>> {
1660 let w = self.writer.lock().expect("txlog writer");
1661 if let Some(cause) = self.poisoned_by() {
1662 return Err(Error::Poisoned {
1663 dir: self.dir.clone(),
1664 cause,
1665 });
1666 }
1667 if self.released.load(Ordering::Acquire) {
1668 return Err(Error::Released(self.dir.clone()));
1669 }
1670 Ok(w)
1671 }
1672
1673 pub fn release(&self) -> Result<()> {
1685 let mut w = self.writer.lock().expect("txlog writer");
1686 if self.released.load(Ordering::Acquire) {
1687 return Ok(());
1688 }
1689 self.publish(&mut w)?;
1690 w.file.sync_all()?;
1691 w.idx.sync_all()?;
1692 w.file = OpenOptions::new().append(true).open("/dev/null")?;
1693 w.idx = OpenOptions::new().append(true).open("/dev/null")?;
1694 self.released.store(true, Ordering::Release);
1695 Ok(())
1696 }
1697
1698 pub fn reacquire(&self) -> Result<()> {
1702 let mut w = self.writer.lock().expect("txlog writer");
1703 if !self.released.load(Ordering::Acquire) {
1704 return Ok(());
1705 }
1706 let path = seg_path(&self.dir, w.seg.first);
1707 let mut file = OpenOptions::new().append(true).open(&path)?;
1708 let on_disk = file.metadata()?.len();
1709 if on_disk != w.seg.len {
1710 return Err(Error::Corrupt {
1711 path,
1712 offset: on_disk,
1713 what: "the open segment changed length while the log was released",
1714 });
1715 }
1716 file.seek(SeekFrom::Start(w.seg.len))?;
1717 let idx = OpenOptions::new()
1718 .append(true)
1719 .open(idx_path(&self.dir, w.seg.first))?;
1720 w.file = file;
1721 w.idx = idx;
1722 w.disk_len = w.seg.len;
1723 self.released.store(false, Ordering::Release);
1724 Ok(())
1725 }
1726
1727 pub fn is_released(&self) -> bool {
1729 self.released.load(Ordering::Acquire)
1730 }
1731
1732 fn roll_if_full(&self, w: &mut Writer) -> Result<()> {
1733 if w.seg.len < self.segment_bytes || w.seg.offsets.is_empty() {
1734 return Ok(());
1735 }
1736 let next_first = w.seg.first + w.seg.offsets.len() as u64;
1737 self.publish(w)?;
1739 w.file.sync_all()?;
1740 w.idx.sync_all()?;
1741 let file = File::create(seg_path(&self.dir, next_first))?;
1742 let idx = File::create(idx_path(&self.dir, next_first))?;
1743 sync_dir(&self.dir)?;
1744 w.file = file;
1745 w.idx = idx;
1746 w.disk_len = 0;
1747 w.published = 0;
1748 w.seg = Segment {
1749 first: next_first,
1750 offsets: Vec::new(),
1751 len: 0,
1752 };
1753 self.segments.lock().expect("txlog segments").push(Segment {
1754 first: next_first,
1755 offsets: Vec::new(),
1756 len: 0,
1757 });
1758 Ok(())
1759 }
1760
1761 fn locate(&self, seq: u64) -> Result<(PathBuf, u64)> {
1763 let head = self.head_seq();
1764 if seq > head {
1765 return Err(Error::BeyondHead { seq, head });
1766 }
1767 let first = self.first_seq();
1768 if seq < first {
1769 return Err(Error::NotRetained { seq, first });
1770 }
1771 let segs = self.segments.lock().expect("txlog segments");
1772 let i = match segs.binary_search_by(|s| s.first.cmp(&seq)) {
1773 Ok(i) => i,
1774 Err(0) => return Err(Error::NotRetained { seq, first }),
1775 Err(i) => i - 1,
1776 };
1777 let seg = &segs[i];
1778 let k = (seq - seg.first) as usize;
1779 let off = *seg.offsets.get(k).ok_or(Error::BeyondHead { seq, head })?;
1780 Ok((seg_path(&self.dir, seg.first), off))
1781 }
1782
1783 pub fn read(&self, seq: u64) -> Result<Entry> {
1785 let (path, off) = self.locate(seq)?;
1786 let mut file = File::open(&path)?;
1787 file.seek(SeekFrom::Start(off))?;
1788 let mut hdr = [0u8; HEADER_LEN];
1789 file.read_exact(&mut hdr)?;
1790 let h = Header::decode(&hdr).ok_or(Error::Corrupt {
1791 path: path.clone(),
1792 offset: off,
1793 what: "header",
1794 })?;
1795 let mut meta = vec![0u8; h.meta_len as usize];
1796 file.read_exact(&mut meta)?;
1797 if h.kind == Kind::Redacted as u16 {
1798 return Ok(Entry {
1800 seq: h.seq,
1801 kind: h.kind,
1802 meta,
1803 blob: Vec::new(),
1804 });
1805 }
1806 let mut blob = vec![0u8; h.blob_len as usize];
1807 file.read_exact(&mut blob)?;
1808 let mut tr = [0u8; TRAILER_LEN];
1809 file.read_exact(&mut tr)?;
1810 let mut crc = Crc32c::new();
1811 crc.update(&meta);
1812 crc.update(&blob);
1813 if crc.finish() != u32::from_le_bytes(tr) {
1814 return Err(Error::Corrupt {
1815 path,
1816 offset: off,
1817 what: "payload crc",
1818 });
1819 }
1820 Ok(Entry {
1821 seq: h.seq,
1822 kind: h.kind,
1823 meta,
1824 blob,
1825 })
1826 }
1827
1828 pub fn locate_blob(&self, seq: u64) -> Result<Located> {
1831 let (path, off) = self.locate(seq)?;
1832 let mut file = File::open(&path)?;
1833 file.seek(SeekFrom::Start(off))?;
1834 let mut hdr = [0u8; HEADER_LEN];
1835 file.read_exact(&mut hdr)?;
1836 let h = Header::decode(&hdr).ok_or(Error::Corrupt {
1837 path: path.clone(),
1838 offset: off,
1839 what: "header",
1840 })?;
1841 let mut meta = vec![0u8; h.meta_len as usize];
1842 file.read_exact(&mut meta)?;
1843 Ok(Located {
1844 seq: h.seq,
1845 kind: h.kind,
1846 meta,
1847 path,
1848 blob_offset: off + HEADER_LEN as u64 + h.meta_len as u64,
1849 blob_len: if h.kind == Kind::Redacted as u16 {
1851 0
1852 } else {
1853 h.blob_len
1854 },
1855 payload_crc: None,
1856 })
1857 }
1858
1859 pub fn read_pieces<E: From<Error>>(
1873 &self,
1874 seq: u64,
1875 piece: usize,
1876 mut each: impl FnMut(&Located, &[u8], bool) -> std::result::Result<(), E>,
1877 ) -> std::result::Result<Located, E> {
1878 let mut at = self.locate_blob(seq)?;
1879 let frame_at = at.blob_offset - HEADER_LEN as u64 - at.meta.len() as u64;
1880 let corrupt = |path: &Path| Error::Corrupt {
1881 path: path.to_path_buf(),
1882 offset: frame_at,
1883 what: "payload crc",
1884 };
1885 if at.kind == Kind::Redacted as u16 {
1886 each(&at, &[], true)?;
1887 return Ok(at);
1888 }
1889 let mut file = File::open(&at.path).map_err(Error::from)?;
1890 file.seek(SeekFrom::Start(at.blob_offset))
1891 .map_err(Error::from)?;
1892 let mut crc = Crc32c::new();
1893 crc.update(&at.meta);
1894 let mut buf = vec![0u8; (piece.max(1) as u64).min(at.blob_len) as usize];
1895 let mut left = at.blob_len;
1896 loop {
1897 let n = (buf.len() as u64).min(left) as usize;
1898 file.read_exact(&mut buf[..n]).map_err(Error::from)?;
1899 crc.update(&buf[..n]);
1900 left -= n as u64;
1901 if left > 0 {
1902 each(&at, &buf[..n], false)?;
1903 continue;
1904 }
1905 let mut tr = [0u8; TRAILER_LEN];
1906 file.read_exact(&mut tr).map_err(Error::from)?;
1907 if crc.finish() != u32::from_le_bytes(tr) {
1908 return Err(corrupt(&at.path).into());
1909 }
1910 at.payload_crc = Some(u32::from_le_bytes(tr));
1911 each(&at, &buf[..n], true)?;
1912 return Ok(at);
1913 }
1914 }
1915
1916 pub fn read_pieces_from<E: From<Error>>(
1926 &self,
1927 seq: u64,
1928 from: u64,
1929 piece: usize,
1930 mut each: impl FnMut(&Located, &[u8], bool) -> std::result::Result<(), E>,
1931 ) -> std::result::Result<Located, E> {
1932 if from == 0 {
1933 return self.read_pieces(seq, piece, each);
1934 }
1935 let mut at = self.locate_blob(seq)?;
1936 if at.kind == Kind::Redacted as u16 || from > at.blob_len {
1937 return Err(Error::Io(io::Error::new(
1938 io::ErrorKind::InvalidInput,
1939 format!(
1940 "txlog: seq {seq} read from byte {from} of a {}-byte blob",
1941 at.blob_len
1942 ),
1943 ))
1944 .into());
1945 }
1946 let mut file = File::open(&at.path).map_err(Error::from)?;
1947 file.seek(SeekFrom::Start(at.blob_offset + from))
1948 .map_err(Error::from)?;
1949 let mut left = at.blob_len - from;
1950 let mut buf = vec![0u8; (piece.max(1) as u64).min(left.max(1)) as usize];
1951 loop {
1952 let n = (buf.len() as u64).min(left) as usize;
1953 file.read_exact(&mut buf[..n]).map_err(Error::from)?;
1954 left -= n as u64;
1955 if left > 0 {
1956 each(&at, &buf[..n], false)?;
1957 continue;
1958 }
1959 let mut tr = [0u8; TRAILER_LEN];
1960 file.read_exact(&mut tr).map_err(Error::from)?;
1961 at.payload_crc = Some(u32::from_le_bytes(tr));
1962 each(&at, &buf[..n], true)?;
1963 return Ok(at);
1964 }
1965 }
1966
1967 pub fn erase(&self, before: u64, repos: &[String]) -> Result<Erased> {
1988 let _one = self.erase_lock.lock().unwrap_or_else(|p| p.into_inner());
1989 let plan: Vec<(u64, Vec<u64>)> = {
1993 let mut w = self.lock_writer()?;
1994 self.publish(&mut w)?;
1995 self.segments
1996 .lock()
1997 .expect("txlog segments")
1998 .iter()
1999 .filter(|s| !s.offsets.is_empty() && s.first < before)
2000 .map(|s| (s.first, s.offsets.clone()))
2001 .collect()
2002 };
2003 let data = [
2004 Kind::Pack as u16,
2005 Kind::Refs as u16,
2006 Kind::LfsPut as u16,
2007 Kind::LfsDelete as u16,
2008 ];
2009 let mut out = Erased::default();
2010 let mut hdr = [0u8; HEADER_LEN];
2011 for (first, offsets) in plan {
2012 let path = seg_path(&self.dir, first);
2013 let mut file = match OpenOptions::new().read(true).write(true).open(&path) {
2014 Ok(f) => f,
2015 Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
2017 Err(e) => return Err(e.into()),
2018 };
2019 let mut punch: Vec<(u64, u64)> = Vec::new();
2023 for (i, &off) in offsets.iter().enumerate() {
2024 let seq = first + i as u64;
2025 if seq >= before {
2026 break;
2027 }
2028 file.seek(SeekFrom::Start(off))?;
2029 file.read_exact(&mut hdr)?;
2030 let h = Header::decode(&hdr).ok_or(Error::Corrupt {
2031 path: path.clone(),
2032 offset: off,
2033 what: "header",
2034 })?;
2035 let redacted_already = h.kind == Kind::Redacted as u16;
2036 if !redacted_already && !data.contains(&h.kind) {
2037 continue;
2038 }
2039 let mut meta = vec![0u8; h.meta_len as usize];
2040 file.read_exact(&mut meta)?;
2041 let named = Meta::from_bytes(&meta)
2042 .ok()
2043 .and_then(|m| m.repo().map(str::to_owned));
2044 if !named.is_some_and(|r| repos.contains(&r)) {
2045 continue;
2046 }
2047 let blob_at = off + HEADER_LEN as u64 + h.meta_len as u64;
2048 if redacted_already {
2049 punch.push((blob_at, h.blob_len));
2052 continue;
2053 }
2054 let redacted = Header {
2055 kind: Kind::Redacted as u16,
2056 ..h
2057 };
2058 file.seek(SeekFrom::Start(off))?;
2059 file.write_all(&redacted.encode())?;
2060 punch.push((blob_at, h.blob_len));
2061 out.frames += 1;
2062 out.bytes += h.blob_len;
2063 }
2064 if punch.is_empty() {
2065 continue;
2066 }
2067 file.sync_data()?;
2068 self.erase_syncs.fetch_add(1, Ordering::Relaxed);
2069 #[cfg(test)]
2070 self.erase_gate();
2071 for (at, len) in punch {
2073 zero_range(&mut file, at, len)?;
2074 }
2075 file.sync_all()?;
2076 self.erase_syncs.fetch_add(1, Ordering::Relaxed);
2077 }
2078 Ok(out)
2079 }
2080
2081 pub fn erase_syncs(&self) -> u64 {
2083 self.erase_syncs.load(Ordering::Relaxed)
2084 }
2085
2086 #[cfg(test)]
2088 fn hold_next_erase(&self, gate: std::sync::mpsc::Receiver<()>) {
2089 self.erase_gates.lock().unwrap().push_back(gate);
2090 }
2091
2092 #[cfg(test)]
2093 fn erase_gate(&self) {
2094 let gate = self
2095 .erase_gates
2096 .lock()
2097 .unwrap_or_else(|p| p.into_inner())
2098 .pop_front();
2099 if let Some(gate) = gate {
2100 let _ = gate.recv();
2101 }
2102 }
2103
2104 pub fn scan_meta(&self, mut each: impl FnMut(u64, u16, &[u8])) -> Result<()> {
2113 let head = self.head_seq();
2114 let plan: Vec<(u64, u64, u64)> = self
2115 .segments
2116 .lock()
2117 .expect("txlog segments")
2118 .iter()
2119 .filter(|s| !s.offsets.is_empty())
2120 .map(|s| (s.first, s.offsets[0], s.offsets.len() as u64))
2121 .collect();
2122 let mut hdr = [0u8; HEADER_LEN];
2123 let mut meta: Vec<u8> = Vec::with_capacity(256);
2124 for (first, start, n) in plan {
2125 let path = seg_path(&self.dir, first);
2126 let mut r = io::BufReader::with_capacity(1 << 16, File::open(&path)?);
2127 r.seek(SeekFrom::Start(start))?;
2128 let mut offset = start;
2129 for seq in first..first + n {
2130 if seq > head {
2131 return Ok(());
2132 }
2133 r.read_exact(&mut hdr)?;
2134 let h = match Header::decode(&hdr) {
2135 Some(h) if h.seq == seq => h,
2136 Some(_) => {
2137 return Err(Error::Corrupt {
2138 path,
2139 offset,
2140 what: "sequence out of place",
2141 })
2142 }
2143 None => {
2144 return Err(Error::Corrupt {
2145 path,
2146 offset,
2147 what: "header",
2148 })
2149 }
2150 };
2151 meta.resize(h.meta_len as usize, 0);
2152 r.read_exact(&mut meta)?;
2153 each(seq, h.kind, &meta);
2154 r.seek_relative((h.blob_len + TRAILER_LEN as u64) as i64)?;
2155 offset += h.frame_len();
2156 }
2157 }
2158 Ok(())
2159 }
2160
2161 pub fn range(&self, from: u64, to: u64) -> Result<Vec<Entry>> {
2163 let to = to.min(self.head_seq());
2164 let mut out = Vec::new();
2165 let mut seq = from;
2166 while seq <= to {
2167 out.push(self.read(seq)?);
2168 seq += 1;
2169 }
2170 Ok(out)
2171 }
2172
2173 pub fn truncate_before(&self, seq: u64) -> Result<u64> {
2176 let _w = self.lock_writer()?;
2177 let mut segs = self.segments.lock().expect("txlog segments");
2178 while segs.len() > 1 {
2179 let drop = match segs[0].last_seq() {
2180 Some(last) => last < seq,
2181 None => true,
2182 };
2183 if !drop {
2184 break;
2185 }
2186 let first = segs[0].first;
2187 fs::remove_file(seg_path(&self.dir, first))?;
2188 let _ = fs::remove_file(idx_path(&self.dir, first));
2189 segs.remove(0);
2190 }
2191 sync_dir(&self.dir)?;
2192 let first = segs[0].first;
2193 self.first.store(first, Ordering::Release);
2194 Ok(first)
2195 }
2196
2197 pub fn retain_from(&self, seq: u64) -> Result<u64> {
2203 {
2204 let mut w = self.writer.lock().expect("txlog writer");
2205 self.publish(&mut w)?;
2206 if !w.seg.offsets.is_empty() {
2207 let next_first = w.seg.first + w.seg.offsets.len() as u64;
2208 w.file.sync_all()?;
2209 w.idx.sync_all()?;
2210 let file = File::create(seg_path(&self.dir, next_first))?;
2211 let idx = File::create(idx_path(&self.dir, next_first))?;
2212 sync_dir(&self.dir)?;
2213 w.file = file;
2214 w.idx = idx;
2215 w.disk_len = 0;
2216 w.published = 0;
2217 w.seg = Segment {
2218 first: next_first,
2219 offsets: Vec::new(),
2220 len: 0,
2221 };
2222 self.segments.lock().expect("txlog segments").push(Segment {
2223 first: next_first,
2224 offsets: Vec::new(),
2225 len: 0,
2226 });
2227 }
2228 }
2229 self.truncate_before(seq)
2230 }
2231
2232 pub fn reset(&self, keep_at_most: u64) -> Result<()> {
2238 let head = self.head_seq();
2239 if head > keep_at_most {
2240 return Err(Error::OutOfOrder {
2241 got: head,
2242 expected: keep_at_most,
2243 });
2244 }
2245 self.restart_after(0)
2246 }
2247
2248 pub fn restart_after(&self, at: u64) -> Result<()> {
2252 let mut w = self.lock_writer()?;
2253 let mut segs = self.segments.lock().expect("txlog segments");
2254 for s in segs.iter() {
2255 fs::remove_file(seg_path(&self.dir, s.first))?;
2256 let _ = fs::remove_file(idx_path(&self.dir, s.first));
2257 }
2258 segs.clear();
2259 let first = at + 1;
2260 File::create(seg_path(&self.dir, first))?;
2261 File::create(idx_path(&self.dir, first))?;
2262 sync_dir(&self.dir)?;
2263 segs.push(Segment {
2264 first,
2265 offsets: Vec::new(),
2266 len: 0,
2267 });
2268 w.file = OpenOptions::new()
2269 .append(true)
2270 .open(seg_path(&self.dir, first))?;
2271 w.idx = OpenOptions::new()
2272 .append(true)
2273 .open(idx_path(&self.dir, first))?;
2274 w.seg = Segment {
2275 first,
2276 offsets: Vec::new(),
2277 len: 0,
2278 };
2279 w.disk_len = 0;
2280 w.buf.clear();
2281 w.published = 0;
2282 self.head.store(at, Ordering::Release);
2283 self.first.store(first, Ordering::Release);
2284 Ok(())
2285 }
2286
2287 pub fn layout(&self) -> Vec<(u64, u64, u64)> {
2293 self.segments
2294 .lock()
2295 .expect("txlog segments")
2296 .iter()
2297 .map(|s| (s.first, s.first + s.offsets.len() as u64 - 1, s.len))
2298 .collect()
2299 }
2300
2301 pub fn bytes(&self) -> u64 {
2303 self.segments
2304 .lock()
2305 .expect("txlog segments")
2306 .iter()
2307 .map(|s| s.len)
2308 .sum()
2309 }
2310}
2311
2312pub struct Pending<'a> {
2322 log: &'a Log,
2323 w: std::sync::MutexGuard<'a, Writer>,
2324 header: Header,
2325 crc: Crc32c,
2326 written: u64,
2327 finished: bool,
2329}
2330
2331impl Pending<'_> {
2332 pub fn seq(&self) -> u64 {
2334 self.header.seq
2335 }
2336
2337 pub fn remaining(&self) -> u64 {
2339 self.header.blob_len - self.written
2340 }
2341
2342 pub fn write(&mut self, chunk: &[u8]) -> Result<()> {
2344 if chunk.len() as u64 > self.remaining() {
2345 return Err(Error::Io(io::Error::new(
2346 io::ErrorKind::InvalidInput,
2347 "txlog: chunk exceeds the declared blob length",
2348 )));
2349 }
2350 self.w.put(chunk)?;
2351 self.crc.update(chunk);
2352 self.written += chunk.len() as u64;
2353 Ok(())
2354 }
2355
2356 pub fn commit(mut self) -> Result<u64> {
2359 let seq = self.close()?;
2360 self.log.publish(&mut self.w)?;
2361 Ok(seq)
2362 }
2363
2364 pub fn commit_unsynced(mut self) -> Result<u64> {
2374 self.close()
2375 }
2376
2377 fn close(&mut self) -> Result<u64> {
2379 if self.remaining() != 0 {
2380 let owed = self.remaining();
2381 self.abort_inner()?;
2382 return Err(Error::Io(io::Error::new(
2383 io::ErrorKind::UnexpectedEof,
2384 format!("txlog: blob short by {owed} bytes at commit"),
2385 )));
2386 }
2387 let tr = self.crc.finish().to_le_bytes();
2388 self.w.put(&tr)?;
2389 self.finished = true;
2391 let off = self.w.seg.len;
2392 self.w.seg.offsets.push(off);
2393 self.w.seg.len += self.header.frame_len();
2394 Ok(self.header.seq)
2395 }
2396
2397 pub fn abort(mut self) -> Result<()> {
2399 self.abort_inner()
2400 }
2401
2402 fn abort_inner(&mut self) -> Result<()> {
2403 self.finished = true;
2404 if let Err(e) = self.w.cut_to_committed() {
2405 self.log.poison(format!(
2407 "a partial frame could not be cut off the segment ({e})"
2408 ));
2409 return Err(e.into());
2410 }
2411 Ok(())
2412 }
2413}
2414
2415impl Drop for Pending<'_> {
2416 fn drop(&mut self) {
2417 if self.finished {
2418 return;
2419 }
2420 let _ = self.abort_inner();
2423 }
2424}
2425
2426pub fn is_cache_path(rel: &str) -> bool {
2432 let rel = rel.trim_start_matches("./");
2433 let rel = rel
2436 .strip_prefix(".serialdat/")
2437 .or_else(|| rel.strip_prefix("serialdat/"))
2438 .unwrap_or(rel);
2439 let first = rel.split('/').next().unwrap_or("");
2440 let name = rel.rsplit('/').next().unwrap_or("");
2441 matches!(first, ".korp-disktest" | "gc" | "quarantine" | "meta" | "serialdat")
2442 || rel.split('/').any(|c| c.starts_with("objects.exploded"))
2443 || name == "objects.graph"
2445 || name.ends_with(".tmp")
2446 || name.starts_with("incoming-")
2447 || name == "grow-journal"
2448}
2449
2450pub const AUDIT_REL: &str = "log/audit-chain.jsonl";
2457
2458pub const AUDIT_LOCAL_REL: &str = "log/audit-chain.local.jsonl";
2471
2472pub const HUB_CONF_REL: &str = "wg/hub.conf";
2483
2484pub const HUB_CONF_FRAMED_REL: &str = "wg/hub.conf.framed";
2488
2489pub const WG_IDENTITY_KEY_REL: &str = "wg/identity.key";
2500pub const WG_IDENTITY_PUB_REL: &str = "wg/identity.pub";
2502
2503pub fn is_per_box_path(rel: &str) -> bool {
2513 let rel = rel.trim_start_matches("./");
2514 let rel = rel
2517 .strip_prefix(".serialdat/")
2518 .or_else(|| rel.strip_prefix("serialdat/"))
2519 .unwrap_or(rel);
2520 if rel == AUDIT_REL || rel == HUB_CONF_REL {
2521 return false;
2522 }
2523 if matches!(
2526 rel,
2527 WG_IDENTITY_KEY_REL | WG_IDENTITY_PUB_REL | HUB_CONF_FRAMED_REL
2528 ) {
2529 return true;
2530 }
2531 let first = rel.split('/').next().unwrap_or("");
2532 matches!(
2535 first,
2536 "crash" | "log" | "twin.json" | "twin.key" | "txlog-restoring" | "txlog-fresh-docs.json" | "txlog-reseal" | "txlog-snapshot-installing" | "txlog-snapshot-base" | "txlog-snapshot-base.old"
2537 | "txlog-snapshot-dropped" | "txlog-snapshot-too-small"
2538 | "txlog-restore-partial"
2540 | "txlog-restore-snapshot"
2542 )
2543}
2544
2545pub const FRAMED_FILES: &[&str] = &[
2550 "attestations.jsonl",
2551 "transparency-heads.jsonl",
2552 "builds.jsonl",
2553 "build-heads.jsonl",
2554 "forge.jsonl",
2555 "forge-heads.jsonl",
2556 "membership.jsonl",
2557 "membership-heads.jsonl",
2558 "replication.json",
2559 "webhooks.json",
2560 AUDIT_REL,
2563 HUB_CONF_REL,
2566 "identity.json",
2570];
2571
2572pub fn never_framed(rel: &str) -> bool {
2575 is_cache_path(rel) || is_per_box_path(rel)
2576}
2577
2578pub mod sink {
2588 use super::*;
2589
2590 static ROOTS: Mutex<Vec<(PathBuf, Weak<Log>)>> = Mutex::new(Vec::new());
2591
2592 fn refuse_cache(rel: &str) -> Result<()> {
2593 if never_framed(rel) {
2594 return Err(Error::BadMeta(format!(
2595 "{rel} is a cache or per-box path; it is never a transaction"
2596 )));
2597 }
2598 Ok(())
2599 }
2600
2601 pub fn register(root: &Path, log: &Arc<Log>) {
2603 let mut roots = ROOTS.lock().expect("txlog sink");
2604 roots.retain(|(r, w)| w.strong_count() > 0 && r != root);
2605 roots.push((root.to_path_buf(), Arc::downgrade(log)));
2606 }
2607
2608 pub fn log_for(path: &Path) -> Option<(Arc<Log>, String)> {
2610 let roots = ROOTS.lock().expect("txlog sink");
2611 roots
2612 .iter()
2613 .filter(|(r, _)| path.starts_with(r))
2614 .max_by_key(|(r, _)| r.as_os_str().len())
2615 .and_then(|(r, w)| {
2616 let log = w.upgrade()?;
2617 let rel = path.strip_prefix(r).ok()?.to_string_lossy().into_owned();
2618 Some((log, rel))
2619 })
2620 }
2621
2622 #[must_use = "hold the frame until the file has the write"]
2625 pub struct Framed(#[allow(dead_code)] Option<MutationGuard>);
2626
2627 pub fn append_line(path: &Path, line: &str) -> Result<Framed> {
2629 if let Some((log, rel)) = log_for(path) {
2630 refuse_cache(&rel)?;
2631 let guard = log.mutation();
2632 log.append(Kind::Append, &Meta::Append { log: rel }, line.as_bytes())?;
2633 return Ok(Framed(Some(guard)));
2634 }
2635 Ok(Framed(None))
2636 }
2637
2638 #[must_use = "wait for the frame to be durable, then write the line, then drop this"]
2641 pub struct StagedLine {
2642 _guard: MutationGuard,
2643 log: Arc<Log>,
2644 seq: u64,
2645 }
2646
2647 impl StagedLine {
2648 pub fn seq(&self) -> u64 {
2650 self.seq
2651 }
2652 pub fn durable(&self) -> Result<()> {
2655 self.log.sync_through(self.seq)
2656 }
2657 pub fn log(&self) -> &Arc<Log> {
2659 &self.log
2660 }
2661 }
2662
2663 pub fn stage_line(path: &Path, line: &str) -> Result<Option<StagedLine>> {
2667 if let Some((log, rel)) = log_for(path) {
2668 refuse_cache(&rel)?;
2669 if log.is_follower() {
2670 return Err(Error::Follower);
2671 }
2672 let guard = log.mutation();
2673 let seq = log.stage_raw(
2674 Kind::Append as u16,
2675 &Meta::Append { log: rel }.to_bytes(),
2676 line.as_bytes(),
2677 )?;
2678 return Ok(Some(StagedLine {
2679 _guard: guard,
2680 log,
2681 seq,
2682 }));
2683 }
2684 Ok(None)
2685 }
2686
2687 pub fn replace_file(path: &Path, bytes: &[u8], deleted: bool) -> Result<Framed> {
2689 if let Some((log, rel)) = log_for(path) {
2690 refuse_cache(&rel)?;
2691 let guard = log.mutation();
2692 log.append(Kind::File, &Meta::File { path: rel, deleted }, bytes)?;
2693 return Ok(Framed(Some(guard)));
2694 }
2695 Ok(Framed(None))
2696 }
2697}
2698
2699#[cfg(test)]
2700mod tests {
2701 use super::*;
2702
2703 fn meta(repo: &str) -> Meta {
2704 Meta::Pack {
2705 repo: repo.into(),
2706 hash: "sha256".into(),
2707 }
2708 }
2709
2710 #[test]
2716 fn a_failed_fsync_poisons_the_log_until_it_is_reopened() {
2717 let dir = tempfile::tempdir().unwrap();
2718 let m = meta("g/r").to_bytes();
2719 {
2720 let log = Log::open(dir.path()).unwrap();
2721 assert_eq!(log.append_raw(Kind::Pack as u16, &m, b"one").unwrap(), 1);
2722 log.fail_next_fsyncs(1);
2723 log.append_raw(Kind::Pack as u16, &m, b"two")
2724 .expect_err("its fsync failed");
2725 let next = log.append_raw(Kind::Pack as u16, &m, b"three");
2726 assert!(next.is_err(), "an append after a failed fsync was taken (and published the failed frame with it): {next:?}");
2727 assert!(
2728 log.sync().is_err() || log.head_seq() == 1,
2729 "a sync published past the failed fsync"
2730 );
2731 assert_eq!(
2732 log.head_seq(),
2733 1,
2734 "the frame whose fsync failed was published"
2735 );
2736 assert!(
2737 log.read(2).is_err(),
2738 "the frame whose fsync failed is readable"
2739 );
2740 }
2741 let log = Log::open(dir.path()).unwrap();
2742 assert_eq!(
2743 log.head_seq(),
2744 1,
2745 "a reopen resurrected the frame whose fsync failed"
2746 );
2747 assert_eq!(log.append_raw(Kind::Pack as u16, &m, b"again").unwrap(), 2);
2748 assert_eq!(log.read(2).unwrap().blob, b"again");
2749 }
2750
2751 #[test]
2756 fn a_failed_cut_of_a_half_frame_poisons_the_log() {
2757 let dir = tempfile::tempdir().unwrap();
2758 let m = meta("g/r").to_bytes();
2759 {
2760 let log = Log::open(dir.path()).unwrap();
2761 assert_eq!(log.append_raw(Kind::Pack as u16, &m, b"one").unwrap(), 1);
2762 {
2763 let mut w = log.writer.lock().unwrap();
2764 w.faults.fail_put_after = Some(3);
2766 w.faults.fail_cut = true;
2767 }
2768 let big = vec![7u8; DIRECT_BYTES * 2];
2769 log.append_raw(Kind::Pack as u16, &m, &big)
2770 .expect_err("its trailer failed");
2771 let next = log.append_raw(Kind::Pack as u16, &m, b"three");
2772 assert!(
2773 next.is_err(),
2774 "an append after a failed cut landed after the half frame: {next:?}"
2775 );
2776 assert_eq!(log.head_seq(), 1);
2777 }
2778 let log = Log::open(dir.path()).unwrap();
2779 assert_eq!(log.head_seq(), 1);
2780 assert_eq!(log.append_raw(Kind::Pack as u16, &m, b"three").unwrap(), 2);
2781 assert_eq!(log.read(2).unwrap().blob, b"three");
2782 }
2783
2784 #[test]
2785 fn crc32c_known_answer() {
2786 assert_eq!(crc32c(b"123456789"), 0xE306_9283);
2788 let mut a = Crc32c::new();
2790 a.update(b"1234");
2791 let mut b = Crc32c::resume(a.state());
2792 b.update(b"56789");
2793 assert_eq!(b.finish(), 0xE306_9283);
2794 }
2795
2796 #[test]
2797 fn appends_are_dense_and_read_back() {
2798 let dir = tempfile::tempdir().unwrap();
2799 let log = Log::open(dir.path()).unwrap();
2800 assert_eq!(log.head_seq(), 0);
2801 assert_eq!(log.first_seq(), 1);
2802 for i in 1..=10u64 {
2803 let s = log
2804 .append(Kind::Pack, &meta("a/b"), &[i as u8; 100])
2805 .unwrap();
2806 assert_eq!(s, i);
2807 }
2808 assert_eq!(log.head_seq(), 10);
2809 let e = log.read(7).unwrap();
2810 assert_eq!(e.seq, 7);
2811 assert_eq!(e.kind(), Some(Kind::Pack));
2812 assert_eq!(e.blob, vec![7u8; 100]);
2813 assert_eq!(e.meta().unwrap(), meta("a/b"));
2814 assert!(matches!(log.read(11), Err(Error::BeyondHead { .. })));
2815 assert!(matches!(log.read(0), Err(Error::NotRetained { .. })));
2816 }
2817
2818 #[test]
2819 fn reopen_keeps_head_and_trusts_index() {
2820 let dir = tempfile::tempdir().unwrap();
2821 {
2822 let log = Log::open(dir.path()).unwrap();
2823 for i in 0..5u64 {
2824 log.append(Kind::Refs, &meta("x/y"), &[i as u8]).unwrap();
2825 }
2826 }
2827 let log = Log::open(dir.path()).unwrap();
2828 assert_eq!(log.head_seq(), 5);
2829 assert_eq!(log.read(5).unwrap().blob, vec![4u8]);
2830 assert_eq!(log.append(Kind::Refs, &meta("x/y"), b"6").unwrap(), 6);
2831 }
2832
2833 #[test]
2834 fn torn_tail_is_cut_and_the_log_goes_on() {
2835 let dir = tempfile::tempdir().unwrap();
2836 {
2837 let log = Log::open(dir.path()).unwrap();
2838 log.append(Kind::Pack, &meta("a/b"), &[1u8; 1000]).unwrap();
2839 log.append(Kind::Pack, &meta("a/b"), &[2u8; 1000]).unwrap();
2840 }
2841 let path = seg_path(dir.path(), 1);
2842 let len = fs::metadata(&path).unwrap().len();
2843 let f = OpenOptions::new().write(true).open(&path).unwrap();
2845 f.set_len(len - 100).unwrap();
2846 let log = Log::open(dir.path()).unwrap();
2847 assert_eq!(log.head_seq(), 1, "the torn second frame is gone");
2848 assert_eq!(
2849 fs::metadata(&path).unwrap().len(),
2850 len - 1000 - 32 - 4 - meta("a/b").to_bytes().len() as u64
2851 );
2852 assert_eq!(log.append(Kind::Pack, &meta("a/b"), &[3u8; 10]).unwrap(), 2);
2853 drop(log);
2854 let log = Log::open(dir.path()).unwrap();
2855 assert_eq!(log.read(2).unwrap().blob, vec![3u8; 10]);
2856 }
2857
2858 fn unsynced(log: &Log, seq: u64, blob: &[u8]) -> u64 {
2860 let m = meta("g/r").to_bytes();
2861 let mut p = log
2862 .begin(Some(seq), Kind::Pack as u16, &m, blob.len() as u64)
2863 .unwrap();
2864 p.write(blob).unwrap();
2865 p.commit_unsynced().unwrap()
2866 }
2867
2868 #[test]
2873 fn a_group_is_invisible_until_synced_and_then_durable_as_one() {
2874 let dir = tempfile::tempdir().unwrap();
2875 let log = Log::open(dir.path()).unwrap();
2876 log.set_follower(true);
2877 for seq in 1..=5u64 {
2878 assert_eq!(unsynced(&log, seq, &[seq as u8; 100]), seq);
2879 }
2880 assert_eq!(log.head_seq(), 0, "nothing acknowledged before the sync");
2881 assert_eq!(log.unsynced(), 5);
2882 assert!(
2883 matches!(log.read(1), Err(Error::BeyondHead { .. })),
2884 "a reader does not see a frame that is not durable"
2885 );
2886 assert!(
2887 matches!(
2888 log.begin(Some(5), Kind::Pack as u16, b"{}", 0),
2889 Err(Error::OutOfOrder {
2890 got: 5,
2891 expected: 6
2892 })
2893 ),
2894 "the gap rule counts the group"
2895 );
2896 assert_eq!(
2897 fs::metadata(idx_path(dir.path(), 1)).unwrap().len(),
2898 0,
2899 "the index is written after the data's fsync"
2900 );
2901 assert_eq!(log.sync().unwrap(), 5);
2902 assert_eq!((log.head_seq(), log.unsynced()), (5, 0));
2903 assert_eq!(log.read(3).unwrap().blob, vec![3u8; 100]);
2904 assert_eq!(
2905 fs::metadata(idx_path(dir.path(), 1)).unwrap().len(),
2906 5 * 8,
2907 "one index entry per frame"
2908 );
2909 assert_eq!(log.sync().unwrap(), 5, "nothing pending: nothing done");
2910 drop(log);
2911 let log = Log::open(dir.path()).unwrap();
2912 assert_eq!(log.head_seq(), 5);
2913 assert_eq!(log.read(5).unwrap().blob, vec![5u8; 100]);
2914 }
2915
2916 #[test]
2921 fn a_crash_before_the_sync_loses_only_what_was_never_acknowledged() {
2922 let dir = tempfile::tempdir().unwrap();
2923 let log = Log::open(dir.path()).unwrap();
2924 for seq in 1..=3u64 {
2925 unsynced(&log, seq, b"acked");
2926 }
2927 assert_eq!(log.sync().unwrap(), 3);
2928 unsynced(&log, 4, b"staged");
2930 unsynced(&log, 5, b"staged");
2931 drop(log);
2932 let log = Log::open(dir.path()).unwrap();
2933 assert_eq!(
2934 log.head_seq(),
2935 3,
2936 "the acknowledged group is whole; the staged one never reached the file"
2937 );
2938 let big = vec![7u8; DIRECT_BYTES * 2];
2940 unsynced(&log, 4, &big);
2941 let m = meta("g/r").to_bytes();
2942 let mut half = log
2943 .begin(Some(5), Kind::Pack as u16, &m, (DIRECT_BYTES * 4) as u64)
2944 .unwrap();
2945 half.write(&big).unwrap();
2946 std::mem::forget(half); let path = seg_path(dir.path(), 1);
2948 let on_disk = fs::metadata(&path).unwrap().len();
2949 drop(log);
2950 let log = Log::open(dir.path()).unwrap();
2951 assert_eq!(
2952 log.head_seq(),
2953 4,
2954 "the whole frame that reached the file is kept, the half one is cut"
2955 );
2956 assert_eq!(log.read(4).unwrap().blob, big);
2957 assert!(
2958 fs::metadata(&path).unwrap().len() < on_disk,
2959 "the half frame is gone from the segment"
2960 );
2961 assert_eq!(log.append(Kind::Pack, &meta("g/r"), b"next").unwrap(), 5);
2962 }
2963
2964 #[test]
2967 fn an_abort_inside_a_group_keeps_the_frames_committed_before_it() {
2968 let dir = tempfile::tempdir().unwrap();
2969 let log = Log::open(dir.path()).unwrap();
2970 unsynced(&log, 1, b"one");
2971 unsynced(&log, 2, &vec![2u8; DIRECT_BYTES]);
2972 let m = meta("g/r").to_bytes();
2973 {
2974 let mut p = log.begin(Some(3), Kind::Pack as u16, &m, 10).unwrap();
2975 p.write(b"abc").unwrap();
2976 }
2978 {
2979 let mut p = log
2980 .begin(Some(3), Kind::Pack as u16, &m, (DIRECT_BYTES * 2) as u64)
2981 .unwrap();
2982 p.write(&vec![9u8; DIRECT_BYTES]).unwrap();
2983 p.abort().unwrap();
2984 }
2985 assert_eq!(log.sync().unwrap(), 2);
2986 assert_eq!(log.read(2).unwrap().blob, vec![2u8; DIRECT_BYTES]);
2987 assert_eq!(unsynced(&log, 3, b"three"), 3);
2988 assert_eq!(log.sync().unwrap(), 3);
2989 drop(log);
2990 let log = Log::open(dir.path()).unwrap();
2991 assert_eq!(log.head_seq(), 3);
2992 assert_eq!(log.read(3).unwrap().blob, b"three".to_vec());
2993 }
2994
2995 #[test]
3010 fn a_waiter_whose_frame_is_durable_returns_without_waiting_for_the_next_leaders_fsync() {
3011 use std::sync::mpsc::channel;
3012 use std::time::Duration;
3013 let dir = tempfile::tempdir().unwrap();
3014 let log = Arc::new(Log::open(dir.path()).unwrap());
3015 let m = meta("g/r").to_bytes();
3016 assert_eq!(log.stage_raw(Kind::Pack as u16, &m, b"one").unwrap(), 1);
3017 assert_eq!(log.stage_raw(Kind::Pack as u16, &m, b"x").unwrap(), 2);
3018 let (open_a, gate_a) = channel::<()>();
3019 let (open_b, gate_b) = channel::<()>();
3020 log.hold_next_fsync(gate_a);
3021 log.hold_next_fsync(gate_b);
3022 let gates_left = |log: &Log| log.fsync_gates.lock().unwrap().len();
3023
3024 let a = {
3026 let log = Arc::clone(&log);
3027 std::thread::spawn(move || log.sync_through(1).unwrap())
3028 };
3029 while gates_left(&log) != 1 {
3030 std::thread::sleep(Duration::from_millis(5));
3031 }
3032 assert_eq!(log.stage_raw(Kind::Pack as u16, &m, b"three").unwrap(), 3);
3034 let b = {
3035 let log = Arc::clone(&log);
3036 std::thread::spawn(move || log.sync_through(3).unwrap())
3037 };
3038 std::thread::sleep(Duration::from_millis(200));
3039 let (x_done, x_said) = channel::<()>();
3040 let x = {
3041 let log = Arc::clone(&log);
3042 std::thread::spawn(move || {
3043 log.sync_through(2).unwrap();
3044 let _ = x_done.send(());
3045 })
3046 };
3047 std::thread::sleep(Duration::from_millis(200));
3048
3049 open_a.send(()).unwrap();
3051 a.join().unwrap();
3052 assert!(log.head_seq() >= 2, "A published frames 1 and 2");
3053 let t = std::time::Instant::now();
3055 while gates_left(&log) != 0 && t.elapsed() < Duration::from_secs(5) {
3056 std::thread::sleep(Duration::from_millis(5));
3057 }
3058 let held = gates_left(&log) == 0;
3059 let back = x_said.recv_timeout(Duration::from_secs(5));
3060 open_b.send(()).unwrap();
3061 b.join().unwrap();
3062 x.join().unwrap();
3063 assert!(held, "B led frame 3's fsync (its gate was taken)");
3064 assert!(
3065 back.is_ok(),
3066 "X's frame was durable since A published it, yet X waited behind B's fsync of frame 3"
3067 );
3068 assert_eq!(log.head_seq(), 3);
3069 }
3070
3071 #[test]
3079 fn an_erase_is_two_fsyncs_per_segment_and_appends_do_not_wait_for_it() {
3080 use std::sync::mpsc::channel;
3081 use std::time::Duration;
3082 let dir = tempfile::tempdir().unwrap();
3083 let log = Arc::new(Log::open_with(dir.path(), 64 << 10).unwrap());
3084 for i in 0..200u64 {
3085 let repo = if i % 2 == 0 { "gone/a" } else { "stays/b" };
3086 log.append(Kind::Pack, &meta(repo), &vec![i as u8; 1500])
3087 .unwrap();
3088 log.append(Kind::Pack, &meta("gone/a"), &vec![7u8; 600])
3089 .unwrap();
3090 }
3091 let before = log.append(Kind::Refs, &meta("stays/b"), b"marker").unwrap();
3092 let segments = log.segments.lock().unwrap().len() as u64;
3093 let syncs0 = log.erase_syncs();
3094
3095 let (open, gate) = channel::<()>();
3097 log.hold_next_erase(gate);
3098 let eraser = {
3099 let log = Arc::clone(&log);
3100 std::thread::spawn(move || log.erase(before, &["gone/a".to_string()]).unwrap())
3101 };
3102 while !log.erase_gates.lock().unwrap().is_empty() {
3103 std::thread::sleep(Duration::from_millis(5));
3104 }
3105 let (done, said) = channel::<u64>();
3106 let appender = {
3107 let log = Arc::clone(&log);
3108 std::thread::spawn(move || {
3109 let s = log.append(Kind::Refs, &meta("stays/b"), b"during").unwrap();
3110 let _ = done.send(s);
3111 })
3112 };
3113 let appended = said.recv_timeout(Duration::from_secs(3));
3114 open.send(()).unwrap();
3115 let erased = eraser.join().unwrap();
3116 appender.join().unwrap();
3117 let syncs = log.erase_syncs() - syncs0;
3118 let line = format!(
3119 "erased {} frames in {segments} segments with {syncs} fsyncs",
3120 erased.frames
3121 );
3122 eprintln!("{line}");
3123 assert_eq!(erased.frames, 300, "{line}");
3124 assert!(appended.is_ok(), "an append waited for the erase: {line}");
3125 assert!(
3126 syncs <= 2 * segments,
3127 "about two fsyncs per segment: {line}"
3128 );
3129 for seq in 1..before {
3131 let e = log.read(seq).unwrap();
3132 let gone = Meta::from_bytes(&e.meta).unwrap().repo() == Some("gone/a");
3133 assert_eq!(e.kind == Kind::Redacted as u16, gone, "seq {seq}");
3134 assert_eq!(e.blob.is_empty(), gone, "seq {seq}");
3135 }
3136 }
3137
3138 #[test]
3143 fn a_group_across_a_segment_roll_and_a_synced_append_after_it() {
3144 let dir = tempfile::tempdir().unwrap();
3145 let log = Log::open_with(dir.path(), 4096).unwrap();
3146 for seq in 1..=40u64 {
3147 unsynced(&log, seq, &vec![seq as u8; 300]);
3148 }
3149 assert!(
3150 log.head_seq() > 0,
3151 "the rolls published the groups in the closed segments"
3152 );
3153 assert_eq!(log.sync().unwrap(), 40);
3154 unsynced(&log, 41, b"group");
3155 assert_eq!(
3156 log.append(Kind::Pack, &meta("g/r"), b"synced").unwrap(),
3157 42,
3158 "the append comes after the group"
3159 );
3160 assert_eq!(log.head_seq(), 42);
3161 for seq in 1..=40u64 {
3162 assert_eq!(
3163 log.read(seq).unwrap().blob,
3164 vec![seq as u8; 300],
3165 "seq {seq}"
3166 );
3167 }
3168 drop(log);
3169 let log = Log::open_with(dir.path(), 4096).unwrap();
3170 assert_eq!(log.head_seq(), 42);
3171 assert_eq!(log.read(41).unwrap().blob, b"group".to_vec());
3172 assert_eq!(log.read(42).unwrap().blob, b"synced".to_vec());
3173 }
3174
3175 #[test]
3176 fn torn_header_is_cut_too() {
3177 let dir = tempfile::tempdir().unwrap();
3178 {
3179 let log = Log::open(dir.path()).unwrap();
3180 log.append(Kind::Pack, &meta("a/b"), &[1u8; 10]).unwrap();
3181 }
3182 let path = seg_path(dir.path(), 1);
3183 let mut f = OpenOptions::new().append(true).open(&path).unwrap();
3184 f.write_all(b"GTX1\x05\x00\x00").unwrap(); let log = Log::open(dir.path()).unwrap();
3186 assert_eq!(log.head_seq(), 1);
3187 assert_eq!(log.append(Kind::Pack, &meta("a/b"), &[2u8; 10]).unwrap(), 2);
3188 }
3189
3190 #[test]
3193 fn scan_meta_agrees_with_locate_blob_across_segments_and_retention() {
3194 let dir = tempfile::tempdir().unwrap();
3195 let log = Log::open_with(dir.path(), 4096).unwrap();
3196 for i in 0..200u64 {
3197 let blob = vec![i as u8; (i as usize * 37) % 9000];
3198 log.append(Kind::Pack, &meta(&format!("r/{i}")), &blob)
3199 .unwrap();
3200 }
3201 log.retain_from(50).unwrap();
3202 let mut seen = Vec::new();
3203 log.scan_meta(|seq, kind, m| seen.push((seq, kind, m.to_vec())))
3204 .unwrap();
3205 let want: Vec<_> = (log.first_seq()..=log.head_seq())
3206 .map(|s| {
3207 let at = log.locate_blob(s).unwrap();
3208 (at.seq, at.kind, at.meta)
3209 })
3210 .collect();
3211 assert!(log.first_seq() > 1 && !want.is_empty());
3212 assert_eq!(seen, want);
3213 }
3214
3215 #[test]
3216 fn corrupted_payload_in_tail_is_cut() {
3217 let dir = tempfile::tempdir().unwrap();
3218 let first_len;
3219 {
3220 let log = Log::open(dir.path()).unwrap();
3221 log.append(Kind::Pack, &meta("a/b"), &[1u8; 10]).unwrap();
3222 first_len = log.bytes();
3223 log.append(Kind::Pack, &meta("a/b"), &[2u8; 10]).unwrap();
3224 }
3225 let path = seg_path(dir.path(), 1);
3226 let mut f = OpenOptions::new().write(true).open(&path).unwrap();
3227 f.seek(SeekFrom::Start(
3229 first_len + 32 + meta("a/b").to_bytes().len() as u64 + 3,
3230 ))
3231 .unwrap();
3232 f.write_all(&[0xEE]).unwrap();
3233 let log = Log::open(dir.path()).unwrap();
3234 assert_eq!(log.head_seq(), 1);
3235 }
3236
3237 #[test]
3238 fn stale_index_is_rebuilt_from_data() {
3239 let dir = tempfile::tempdir().unwrap();
3240 {
3241 let log = Log::open(dir.path()).unwrap();
3242 for _ in 0..4 {
3243 log.append(Kind::Pack, &meta("a/b"), &[9u8; 10]).unwrap();
3244 }
3245 }
3246 fs::remove_file(idx_path(dir.path(), 1)).unwrap();
3248 fs::write(idx_path(dir.path(), 1), []).unwrap();
3249 let log = Log::open(dir.path()).unwrap();
3250 assert_eq!(log.head_seq(), 4);
3251 drop(log);
3252 fs::write(idx_path(dir.path(), 1), [0u8; 8 * 9]).unwrap();
3253 let log = Log::open(dir.path()).unwrap();
3254 assert_eq!(log.head_seq(), 4);
3255 assert_eq!(log.read(3).unwrap().seq, 3);
3256 }
3257
3258 #[test]
3259 fn segments_roll_and_retention_drops_whole_segments() {
3260 let dir = tempfile::tempdir().unwrap();
3261 let log = Log::open_with(dir.path(), 300).unwrap();
3262 for i in 1..=20u64 {
3263 log.append(Kind::Pack, &meta("a/b"), &[i as u8; 100])
3264 .unwrap();
3265 }
3266 assert!(log.segments.lock().unwrap().len() > 3);
3267 assert_eq!(log.read(1).unwrap().blob[0], 1);
3268 let first = log.truncate_before(10).unwrap();
3269 assert!(first <= 10 && first > 1, "first={first}");
3270 assert!(matches!(log.read(1), Err(Error::NotRetained { .. })));
3271 assert_eq!(log.read(10).unwrap().blob[0], 10);
3272 assert_eq!(log.read(20).unwrap().blob[0], 20);
3273 drop(log);
3274 let log = Log::open_with(dir.path(), 300).unwrap();
3275 assert_eq!(log.first_seq(), first);
3276 assert_eq!(log.head_seq(), 20);
3277 assert_eq!(
3278 log.append(Kind::Pack, &meta("a/b"), &[21u8; 10]).unwrap(),
3279 21
3280 );
3281 }
3282
3283 #[test]
3284 fn append_at_refuses_out_of_order() {
3285 let dir = tempfile::tempdir().unwrap();
3286 let log = Log::open(dir.path()).unwrap();
3287 let m = meta("a/b").to_bytes();
3288 assert_eq!(log.append_at(1, 1, &m, b"x").unwrap(), 1);
3289 assert!(matches!(
3290 log.append_at(3, 1, &m, b"x"),
3291 Err(Error::OutOfOrder {
3292 got: 3,
3293 expected: 2
3294 })
3295 ));
3296 assert!(matches!(
3297 log.append_at(1, 1, &m, b"x"),
3298 Err(Error::OutOfOrder { .. })
3299 ));
3300 assert_eq!(log.append_at(2, 1, &m, b"x").unwrap(), 2);
3301 }
3302
3303 #[test]
3304 fn streaming_append_commits_and_aborts() {
3305 let dir = tempfile::tempdir().unwrap();
3306 let log = Log::open(dir.path()).unwrap();
3307 let m = meta("a/b").to_bytes();
3308 {
3309 let mut p = log.begin(Some(1), 1, &m, 6).unwrap();
3310 p.write(b"abc").unwrap();
3311 p.write(b"def").unwrap();
3312 assert_eq!(p.commit().unwrap(), 1);
3313 }
3314 {
3315 let mut p = log.begin(None, 1, &m, 6).unwrap();
3316 p.write(b"abc").unwrap();
3317 p.abort().unwrap();
3318 }
3319 assert_eq!(log.head_seq(), 1);
3320 {
3321 let mut p = log.begin(None, 1, &m, 3).unwrap();
3322 p.write(b"xy").unwrap();
3323 assert!(p.commit().is_err(), "short blob is refused");
3324 }
3325 assert_eq!(log.head_seq(), 1);
3326 assert_eq!(log.append_at(2, 1, &m, b"ok").unwrap(), 2);
3327 drop(log);
3328 let log = Log::open(dir.path()).unwrap();
3329 assert_eq!(log.head_seq(), 2);
3330 assert_eq!(log.read(1).unwrap().blob, b"abcdef");
3331 assert_eq!(log.read(2).unwrap().blob, b"ok");
3332 let l = log.locate_blob(1).unwrap();
3333 assert_eq!(l.blob_len, 6);
3334 let mut f = File::open(&l.path).unwrap();
3335 f.seek(SeekFrom::Start(l.blob_offset)).unwrap();
3336 let mut b = vec![0u8; 6];
3337 f.read_exact(&mut b).unwrap();
3338 assert_eq!(b, b"abcdef");
3339 }
3340
3341 #[test]
3347 fn a_streaming_append_dropped_half_way_leaves_nothing_and_the_next_append_reads_back() {
3348 let dir = tempfile::tempdir().unwrap();
3349 let log = Log::open(dir.path()).unwrap();
3350 let m = meta("a/b").to_bytes();
3351 assert_eq!(log.append_at(1, 1, &m, b"first").unwrap(), 1);
3352 let before = log.bytes();
3353 {
3354 let mut p = log.begin(Some(2), 1, &m, 1 << 20).unwrap();
3355 p.write(&[7u8; 4096]).unwrap();
3356 }
3358 assert_eq!(log.head_seq(), 1, "the dropped frame is not in the log");
3359 let seg = log.locate_blob(1).unwrap().path;
3360 assert_eq!(
3361 std::fs::metadata(&seg).unwrap().len(),
3362 before,
3363 "and not in the segment file either"
3364 );
3365 assert_eq!(log.append_at(2, 1, &m, b"second").unwrap(), 2);
3366 assert_eq!(
3367 log.read(2).unwrap().blob,
3368 b"second",
3369 "the next append reads back in this process"
3370 );
3371 drop(log);
3372 let log = Log::open(dir.path()).unwrap();
3373 assert_eq!(log.head_seq(), 2);
3374 assert_eq!(log.read(1).unwrap().blob, b"first");
3375 assert_eq!(log.read(2).unwrap().blob, b"second", "and after a restart");
3376 }
3377
3378 #[test]
3384 fn read_pieces_streams_the_blob_and_withholds_the_last_piece_of_a_corrupt_frame() {
3385 let dir = tempfile::tempdir().unwrap();
3386 let log = Log::open(dir.path()).unwrap();
3387 let blob: Vec<u8> = (0..10_000u32).map(|i| (i * 7 % 251) as u8).collect();
3388 let s = log.append(Kind::Pack, &meta("a/b"), &blob).unwrap();
3389 let empty = log.append(Kind::Refs, &meta("a/b"), b"").unwrap();
3390 let rot = log.append(Kind::Pack, &meta("a/c"), &blob).unwrap();
3391 let mut got = Vec::new();
3392 let mut lasts = 0;
3393 let mut want = Crc32c::new();
3395 want.update(&meta("a/b").to_bytes());
3396 want.update(&blob);
3397 let want = want.finish();
3398 let at = log
3399 .read_pieces::<Error>(s, 3000, |l, p, last| {
3400 assert_eq!(l.seq, s);
3401 assert!(p.len() <= 3000);
3402 assert_eq!(
3403 l.payload_crc,
3404 last.then_some(want),
3405 "the crc is handed over with the last piece and only then"
3406 );
3407 got.extend_from_slice(p);
3408 lasts += usize::from(last);
3409 Ok(())
3410 })
3411 .unwrap();
3412 assert_eq!(
3413 (got, lasts, at.blob_len),
3414 (blob.clone(), 1, blob.len() as u64)
3415 );
3416 assert_eq!(at.payload_crc, Some(want));
3417 let mut tail = Vec::new();
3420 let at = log
3421 .read_pieces_from::<Error>(s, 4321, 3000, |l, p, last| {
3422 assert!(p.len() <= 3000);
3423 assert_eq!(l.payload_crc, last.then_some(want));
3424 tail.extend_from_slice(p);
3425 Ok(())
3426 })
3427 .unwrap();
3428 assert_eq!(
3429 (tail.as_slice(), at.payload_crc),
3430 (&blob[4321..], Some(want))
3431 );
3432 let mut end = Vec::new();
3433 log.read_pieces_from::<Error>(s, blob.len() as u64, 3000, |l, p, last| {
3434 end.push((p.len(), last, l.payload_crc));
3435 Ok(())
3436 })
3437 .unwrap();
3438 assert_eq!(end, vec![(0, true, Some(want))]);
3439 assert!(log
3440 .read_pieces_from::<Error>(s, blob.len() as u64 + 1, 3000, |_, _, _| Ok(()))
3441 .is_err());
3442 let mut pieces = Vec::new();
3443 log.read_pieces::<Error>(empty, 3000, |_, p, last| {
3444 pieces.push((p.len(), last));
3445 Ok(())
3446 })
3447 .unwrap();
3448 assert_eq!(pieces, vec![(0, true)]);
3449 let l = log.locate_blob(rot).unwrap();
3451 let mut f = OpenOptions::new()
3452 .read(true)
3453 .write(true)
3454 .open(&l.path)
3455 .unwrap();
3456 f.seek(SeekFrom::Start(l.blob_offset + 5000)).unwrap();
3457 let mut b = [0u8; 1];
3458 f.read_exact(&mut b).unwrap();
3459 f.seek(SeekFrom::Start(l.blob_offset + 5000)).unwrap();
3460 f.write_all(&[b[0] ^ 0xFF]).unwrap();
3461 drop(f);
3462 let mut seen = 0usize;
3463 let mut last_seen = false;
3464 let r = log.read_pieces::<Error>(rot, 3000, |_, p, last| {
3465 seen += p.len();
3466 last_seen |= last;
3467 Ok(())
3468 });
3469 assert!(
3470 matches!(
3471 r,
3472 Err(Error::Corrupt {
3473 what: "payload crc",
3474 ..
3475 })
3476 ),
3477 "{r:?}"
3478 );
3479 assert!(
3480 !last_seen,
3481 "the last piece of a corrupt frame is never handed over"
3482 );
3483 assert!(
3484 seen < blob.len(),
3485 "{seen} of {} bytes went out before the refusal",
3486 blob.len()
3487 );
3488 assert!(
3489 matches!(log.read(rot), Err(Error::Corrupt { .. })),
3490 "read refuses the same frame"
3491 );
3492 log.erase(rot + 1, &["a/b".to_string()]).unwrap();
3494 let mut erased = Vec::new();
3495 log.read_pieces::<Error>(s, 3000, |l, p, last| {
3496 erased.push((l.kind, p.len(), last));
3497 Ok(())
3498 })
3499 .unwrap();
3500 assert_eq!(erased, vec![(Kind::Redacted as u16, 0, true)]);
3501 }
3502
3503 #[test]
3504 fn unknown_kind_is_stored_and_read_back() {
3505 let dir = tempfile::tempdir().unwrap();
3506 let log = Log::open(dir.path()).unwrap();
3507 log.append_raw(999, b"{}", b"future").unwrap();
3508 let e = log.read(1).unwrap();
3509 assert_eq!(e.kind(), None);
3510 assert_eq!(e.blob, b"future");
3511 }
3512
3513 #[test]
3514 fn reset_starts_over_at_one_and_refuses_a_box_with_history() {
3515 let dir = tempfile::tempdir().unwrap();
3516 let log = Log::open(dir.path()).unwrap();
3517 log.append(Kind::Kek, &Meta::Kek {}, &[7u8; 32]).unwrap();
3518 assert!(log.reset(0).is_err());
3520 assert_eq!(log.head_seq(), 1);
3521 log.reset(1).unwrap();
3522 assert_eq!(log.head_seq(), 0);
3523 assert_eq!(log.first_seq(), 1);
3524 log.append_at(1, Kind::Kek as u16, &Meta::Kek {}.to_bytes(), &[9u8; 32])
3526 .unwrap();
3527 drop(log);
3528 let log = Log::open(dir.path()).unwrap();
3529 assert_eq!(log.head_seq(), 1);
3530 assert_eq!(log.read(1).unwrap().blob, vec![9u8; 32]);
3531 }
3532
3533 #[test]
3534 fn kek_append_and_file_kinds_round_trip() {
3535 let dir = tempfile::tempdir().unwrap();
3536 let log = Log::open(dir.path()).unwrap();
3537 let metas = [
3538 (Kind::Kek, Meta::Kek {}),
3539 (
3540 Kind::Append,
3541 Meta::Append {
3542 log: "attestations.jsonl".into(),
3543 },
3544 ),
3545 (
3546 Kind::File,
3547 Meta::File {
3548 path: "replication.json".into(),
3549 deleted: false,
3550 },
3551 ),
3552 ];
3553 for (k, m) in &metas {
3554 log.append(*k, m, b"x").unwrap();
3555 }
3556 for (i, (k, m)) in metas.iter().enumerate() {
3557 let e = log.read(i as u64 + 1).unwrap();
3558 assert_eq!(e.kind(), Some(*k));
3559 assert_eq!(&e.meta().unwrap(), m);
3560 }
3561 }
3562
3563 #[test]
3564 fn a_follower_log_refuses_originations_and_takes_the_primarys_numbers() {
3565 let dir = tempfile::tempdir().unwrap();
3566 let log = Log::open(dir.path()).unwrap();
3567 log.set_follower(true);
3568 assert!(matches!(
3569 log.append(Kind::Pack, &meta("a/b"), b"x"),
3570 Err(Error::Follower)
3571 ));
3572 assert!(matches!(
3573 log.begin(None, Kind::Pack as u16, b"{}", 1),
3574 Err(Error::Follower)
3575 ));
3576 log.append_at(1, Kind::Pack as u16, &meta("a/b").to_bytes(), b"x")
3577 .unwrap();
3578 assert_eq!(log.head_seq(), 1);
3579 log.set_follower(false);
3580 assert_eq!(log.append(Kind::Pack, &meta("a/b"), b"y").unwrap(), 2);
3581 }
3582
3583 #[test]
3584 fn cache_and_per_box_paths_are_judged_by_their_place_in_the_serialdat_tree() {
3585 for p in [
3586 "gc/x",
3587 ".serialdat/gc/x",
3588 "serialdat/quarantine/p.pack",
3589 ".serialdat/meta/identity.json",
3590 "repos/a/b.git/objects.exploded/1",
3591 ".korp-disktest/probe",
3592 "webhooks.json.tmp",
3593 ] {
3594 assert!(is_cache_path(p) && never_framed(p), "{p} is cache");
3595 }
3596 for p in [
3597 "crash/panic-1",
3598 ".serialdat/log/console.txt",
3599 "log/audit-chain.local.jsonl",
3600 ".serialdat/log/audit-chain.local.jsonl",
3601 "twin.key",
3602 ] {
3603 assert!(
3604 !is_cache_path(p) && is_per_box_path(p) && never_framed(p),
3605 "{p} is per box"
3606 );
3607 }
3608 for p in [
3609 "attestations.jsonl",
3610 "replication.json",
3611 "repos/a/b.git/pack",
3612 "control.redb",
3613 ] {
3614 assert!(!never_framed(p), "{p} is a transaction's path");
3615 }
3616 }
3617
3618 #[test]
3627 fn the_audit_trail_is_framed_and_the_twins_own_trail_is_not() {
3628 for p in [
3629 "log/audit-chain.jsonl",
3630 ".serialdat/log/audit-chain.jsonl",
3631 "serialdat/log/audit-chain.jsonl",
3632 "./log/audit-chain.jsonl",
3633 ] {
3634 assert!(
3635 !is_per_box_path(p),
3636 "{p} is the audit trail and travels with the log"
3637 );
3638 assert!(
3639 !never_framed(p),
3640 "{p} must be framable, or a restore loses the audit trail"
3641 );
3642 }
3643 assert!(
3644 FRAMED_FILES.contains(&AUDIT_REL),
3645 "a snapshot must carry the audit trail"
3646 );
3647 assert!(is_per_box_path(AUDIT_LOCAL_REL) && never_framed(AUDIT_LOCAL_REL));
3649 assert!(!FRAMED_FILES.contains(&AUDIT_LOCAL_REL));
3650 }
3651
3652 #[test]
3665 fn the_hub_declaration_travels_and_the_machines_own_identity_never_does() {
3666 for p in [HUB_CONF_REL, ".serialdat/wg/hub.conf", "./wg/hub.conf"] {
3667 assert!(
3668 !never_framed(p),
3669 "{p} must travel: a restored main without it is unreachable"
3670 );
3671 }
3672 assert!(
3673 FRAMED_FILES.contains(&HUB_CONF_REL),
3674 "a snapshot must carry the hub declaration"
3675 );
3676 for p in [
3677 WG_IDENTITY_KEY_REL,
3678 WG_IDENTITY_PUB_REL,
3679 HUB_CONF_FRAMED_REL,
3680 ] {
3681 assert!(
3682 is_per_box_path(p) && never_framed(p),
3683 "{p} is this machine's own and never travels"
3684 );
3685 assert!(!FRAMED_FILES.contains(&p), "{p} must not be in a snapshot");
3686 }
3687 }
3688
3689 #[test]
3694 #[cfg(target_os = "linux")]
3695 fn a_released_log_holds_nothing_under_its_dir_refuses_writes_and_resumes_after_reacquire() {
3696 let root = tempfile::tempdir().unwrap();
3697 let dir = root.path().join("txlog");
3698 let log = Log::open(&dir).unwrap();
3699 let holds = |dir: &Path| -> Vec<PathBuf> {
3700 fs::read_dir("/proc/self/fd")
3701 .unwrap()
3702 .filter_map(|e| fs::read_link(e.ok()?.path()).ok())
3703 .filter(|p| p.starts_with(dir))
3704 .collect()
3705 };
3706 log.append(Kind::Kek, &Meta::Kek {}, b"one").unwrap();
3707 log.append(Kind::Kek, &Meta::Kek {}, b"two").unwrap();
3708 assert!(!holds(&dir).is_empty(), "the writer holds the open segment");
3709 log.release().unwrap();
3710 log.release().unwrap();
3711 assert!(log.is_released());
3712 assert_eq!(
3713 holds(&dir),
3714 Vec::<PathBuf>::new(),
3715 "released: nothing under the dir"
3716 );
3717 assert!(matches!(
3718 log.append(Kind::Kek, &Meta::Kek {}, b"no"),
3719 Err(Error::Released(_))
3720 ));
3721 assert!(matches!(
3722 log.begin(None, 1, b"", 1),
3723 Err(Error::Released(_))
3724 ));
3725 assert_eq!(log.read(2).unwrap().blob, b"two", "readers open per call");
3726 log.reacquire().unwrap();
3727 log.reacquire().unwrap();
3728 assert_eq!(log.append(Kind::Kek, &Meta::Kek {}, b"three").unwrap(), 3);
3729 drop(log);
3730 let back = Log::open(&dir).unwrap();
3731 assert_eq!(back.head_seq(), 3);
3732 assert_eq!(back.read(3).unwrap().blob, b"three");
3733 }
3734
3735 #[test]
3737 fn reacquire_refuses_a_segment_that_changed_length() {
3738 let root = tempfile::tempdir().unwrap();
3739 let dir = root.path().join("txlog");
3740 let log = Log::open(&dir).unwrap();
3741 log.append(Kind::Kek, &Meta::Kek {}, b"one").unwrap();
3742 log.release().unwrap();
3743 OpenOptions::new()
3744 .append(true)
3745 .open(seg_path(&dir, 1))
3746 .unwrap()
3747 .write_all(b"junk")
3748 .unwrap();
3749 assert!(matches!(log.reacquire(), Err(Error::Corrupt { .. })));
3750 assert!(log.is_released(), "still refusing writes");
3751 }
3752
3753 #[test]
3754 fn the_sink_frames_writes_under_a_registered_root_and_nothing_elsewhere() {
3755 let root = tempfile::tempdir().unwrap();
3756 let other = tempfile::tempdir().unwrap();
3757 let log = Arc::new(Log::open(root.path().join("txlog")).unwrap());
3758 sink::register(root.path(), &log);
3759 drop(sink::append_line(&root.path().join("attestations.jsonl"), "{\"a\":1}").unwrap());
3760 drop(sink::replace_file(&root.path().join("webhooks.json"), b"[]", false).unwrap());
3761 drop(sink::append_line(&other.path().join("attestations.jsonl"), "{}").unwrap());
3762 assert_eq!(log.head_seq(), 2);
3763 assert_eq!(
3764 log.read(1).unwrap().meta().unwrap(),
3765 Meta::Append {
3766 log: "attestations.jsonl".into()
3767 }
3768 );
3769 assert_eq!(
3770 log.read(2).unwrap().meta().unwrap(),
3771 Meta::File {
3772 path: "webhooks.json".into(),
3773 deleted: false
3774 }
3775 );
3776 drop(log);
3777 drop(sink::append_line(&root.path().join("attestations.jsonl"), "{}").unwrap());
3779 }
3780
3781 #[test]
3782 fn restart_after_leaves_an_empty_log_whose_next_number_follows_the_snapshot_and_survives_a_reopen(
3783 ) {
3784 let dir = tempfile::tempdir().unwrap();
3785 {
3786 let log = Log::open(dir.path()).unwrap();
3787 log.append(Kind::Pack, &meta("a/b"), b"x").unwrap();
3788 log.restart_after(41).unwrap();
3789 assert_eq!((log.first_seq(), log.head_seq()), (42, 41));
3790 assert!(matches!(
3791 log.append_at(43, Kind::Pack as u16, b"{}", b""),
3792 Err(Error::OutOfOrder { .. })
3793 ));
3794 assert_eq!(
3795 log.append_at(42, Kind::Pack as u16, &meta("a/b").to_bytes(), b"y")
3796 .unwrap(),
3797 42
3798 );
3799 }
3800 let log = Log::open(dir.path()).unwrap();
3801 assert_eq!((log.first_seq(), log.head_seq()), (42, 42));
3802 assert_eq!(log.read(42).unwrap().blob, b"y");
3803 assert!(matches!(log.read(1), Err(Error::NotRetained { .. })));
3804 }
3805
3806 #[test]
3807 fn a_freeze_waits_for_a_mutation_in_flight_and_holds_new_ones_until_it_ends() {
3808 use std::sync::atomic::AtomicU32;
3809 let dir = tempfile::tempdir().unwrap();
3810 let log = Arc::new(Log::open(dir.path()).unwrap());
3811 let stage = Arc::new(AtomicU32::new(0));
3812 let (l, s) = (Arc::clone(&log), Arc::clone(&stage));
3814 let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();
3815 let (in_tx, in_rx) = std::sync::mpsc::channel::<()>();
3816 let writer = std::thread::spawn(move || {
3817 let g = l.mutation();
3818 l.append(Kind::Pack, &meta("a/b"), b"1").unwrap();
3819 in_tx.send(()).unwrap();
3820 go_rx.recv().unwrap();
3821 s.store(1, Ordering::SeqCst); drop(g);
3823 });
3824 in_rx.recv().unwrap();
3825 let (l, s) = (Arc::clone(&log), Arc::clone(&stage));
3826 let (frozen_tx, frozen_rx) = std::sync::mpsc::channel::<()>();
3827 let freezer = std::thread::spawn(move || {
3828 let f = l.freeze();
3829 frozen_tx.send(()).unwrap();
3830 assert_eq!(
3832 s.load(Ordering::SeqCst),
3833 1,
3834 "freeze returned before the mutation in flight ended"
3835 );
3836 let head = l.head_seq();
3837 std::thread::sleep(std::time::Duration::from_millis(50));
3838 assert_eq!(l.head_seq(), head, "no mutation started while frozen");
3839 s.store(2, Ordering::SeqCst);
3840 drop(f);
3841 });
3842 std::thread::sleep(std::time::Duration::from_millis(30));
3843 go_tx.send(()).unwrap();
3844 writer.join().unwrap();
3845 frozen_rx.recv().unwrap();
3847 let g = log.mutation();
3848 assert_eq!(
3849 stage.load(Ordering::SeqCst),
3850 2,
3851 "a mutation started while the log was frozen"
3852 );
3853 log.append(Kind::Pack, &meta("a/b"), b"2").unwrap();
3854 drop(g);
3855 freezer.join().unwrap();
3856 let outer = log.mutation();
3858 let inner = log.mutation();
3859 drop(inner);
3860 drop(outer);
3861 let f = log.freeze();
3862 let own = log.mutation();
3863 drop(own);
3864 drop(f);
3865 }
3866}
3867
3868#[cfg(test)]
3869mod spool_tests {
3870 use super::*;
3874
3875 #[test]
3895 fn the_lfs_spool_directory_is_part_of_the_layout_and_does_not_refuse_the_log() {
3896 let dir = tempfile::tempdir().expect("temp dir");
3897 let log = Log::open(dir.path()).expect("a fresh log opens");
3898 drop(log);
3899 std::fs::create_dir_all(dir.path().join(SPOOL_DIR)).expect("the spool dir");
3900 Log::open(dir.path()).expect("a log whose spool directory exists still opens");
3901 }
3902
3903 #[test]
3907 fn a_stray_entry_is_still_refused_by_name() {
3908 let dir = tempfile::tempdir().expect("temp dir");
3909 Log::open(dir.path()).expect("a fresh log opens");
3910 std::fs::write(dir.path().join("stray"), b"x").expect("write");
3911 let err = Log::open(dir.path()).expect_err("a stray file is not part of the layout");
3912 assert!(matches!(err, Error::Unexpected(_)), "{err}");
3913 }
3914}