use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use crate::error::{Error, IoContext, Result};
const MAGIC: u32 = 0x4d_57_41_4c;
pub const WAL_VERSION: u16 = 1;
const HEADER_LEN: usize = 20;
const CRC_LEN: usize = 4;
const MAX_FRAME_BYTES: u32 = 64 << 20;
const SEGMENT_BYTES: u64 = 64 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum Signal {
Logs = 0,
Traces = 1,
Metrics = 2,
}
impl Signal {
pub const ALL: [Signal; 3] = [Signal::Logs, Signal::Traces, Signal::Metrics];
pub fn as_str(self) -> &'static str {
match self {
Signal::Logs => "logs",
Signal::Traces => "traces",
Signal::Metrics => "metrics",
}
}
pub fn named(s: &str) -> Option<Signal> {
Signal::ALL.into_iter().find(|sig| sig.as_str() == s)
}
fn from_u8(b: u8) -> Option<Signal> {
match b {
0 => Some(Signal::Logs),
1 => Some(Signal::Traces),
2 => Some(Signal::Metrics),
_ => None,
}
}
pub fn index(self) -> usize {
self as usize
}
}
pub type Watermarks = [u64; 3];
struct Inner {
file: File,
path: PathBuf,
written: u64,
next_seq: u64,
dirty: bool,
retired: Vec<(PathBuf, File)>,
}
pub struct Wal {
inner: Mutex<Inner>,
dir: PathBuf,
node: u32,
}
impl Wal {
pub fn open(root: &Path, node: u32) -> Result<Self> {
let dir = root.join(".wal");
fs::create_dir_all(&dir).ctx(&dir)?;
let segments = Self::segments(&dir, node)?;
let mut next_seq = 0;
for (path, first) in &segments {
let mut torn = false;
for frame in FrameReader::open(path)? {
let Ok(frame) = frame else {
torn = true;
break;
};
next_seq = next_seq.max(frame.seq + 1);
}
if torn {
next_seq = next_seq.max(first + 1);
}
}
let path = dir.join(format!("{node:08x}-{next_seq:020}.wal"));
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.ctx(&path)?;
let written = file.metadata().ctx(&path)?.len();
Ok(Wal {
inner: Mutex::new(Inner {
file,
path,
written,
next_seq,
dirty: false,
retired: Vec::new(),
}),
dir,
node,
})
}
pub fn append(&self, signal: Signal, body: &[u8]) -> Result<u64> {
self.append_then(signal, body, |_| {})
}
pub fn append_then(&self, signal: Signal, body: &[u8], then: impl FnOnce(u64)) -> Result<u64> {
let len = u32::try_from(body.len())
.ok()
.filter(|n| *n <= MAX_FRAME_BYTES);
let Some(len) = len else {
return Err(Error::WalFrameTooLarge {
len: body.len(),
max: MAX_FRAME_BYTES,
});
};
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
if inner.written >= SEGMENT_BYTES {
self.roll(&mut inner)?;
}
let seq = inner.next_seq;
let mut header = [0u8; HEADER_LEN];
header[0..4].copy_from_slice(&MAGIC.to_le_bytes());
header[4..6].copy_from_slice(&WAL_VERSION.to_le_bytes());
header[6] = signal as u8;
header[7] = 0;
header[8..16].copy_from_slice(&seq.to_le_bytes());
header[16..20].copy_from_slice(&len.to_le_bytes());
let mut hasher = crc32fast::Hasher::new();
hasher.update(&header);
hasher.update(body);
let crc = hasher.finalize();
inner.file.write_all(&header).ctx(&inner.path)?;
inner.file.write_all(body).ctx(&inner.path)?;
inner.file.write_all(&crc.to_le_bytes()).ctx(&inner.path)?;
inner.written += (HEADER_LEN + body.len() + CRC_LEN) as u64;
inner.next_seq += 1;
inner.dirty = true;
then(seq);
Ok(seq)
}
pub fn sync(&self) -> Result<()> {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
if !inner.dirty && inner.retired.is_empty() {
return Ok(());
}
for (path, file) in std::mem::take(&mut inner.retired) {
crate::sync_data(&file).ctx(&path)?;
}
if inner.dirty {
crate::sync_data(&inner.file).ctx(&inner.path)?;
inner.dirty = false;
}
Ok(())
}
pub fn next_seq(&self) -> u64 {
self.inner
.lock()
.unwrap_or_else(|e| e.into_inner())
.next_seq
}
pub fn truncate(&self, covered: u64) -> Result<usize> {
let current = {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.path.clone()
};
let segments = Self::segments(&self.dir, self.node)?;
let mut removed = 0;
for (path, _) in &segments {
if *path == current {
continue;
}
let mut highest = None;
for frame in FrameReader::open(path)? {
highest = Some(frame?.seq);
}
match highest {
None => {}
Some(hi) if hi < covered => {}
Some(_) => continue,
}
fs::remove_file(path).ctx(path)?;
{
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.retired.retain(|(p, _)| p != path);
}
removed += 1;
}
if removed > 0 {
crate::sync_all(&File::open(&self.dir).ctx(&self.dir)?).ctx(&self.dir)?;
}
Ok(removed)
}
pub fn replay(
root: &Path,
node: u32,
watermarks: Watermarks,
mut f: impl FnMut(Signal, u64, &[u8]) -> Result<()>,
) -> Result<Replayed> {
let dir = root.join(".wal");
if !dir.is_dir() {
return Ok(Replayed::default());
}
let mut out = Replayed::default();
for (path, _) in Self::segments(&dir, node)? {
for frame in FrameReader::open(&path)? {
let Ok(frame) = frame else {
out.torn_segments += 1;
break;
};
if frame.seq < watermarks[frame.signal.index()] {
out.skipped += 1;
continue;
}
f(frame.signal, frame.seq, &frame.body)?;
out.replayed += 1;
out.bytes += frame.body.len() as u64;
}
}
Ok(out)
}
fn segments(dir: &Path, node: u32) -> Result<Vec<(PathBuf, u64)>> {
let prefix = format!("{node:08x}-");
let mut out = Vec::new();
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(out),
Err(e) => {
return Err(Error::Io {
path: dir.into(),
source: e,
});
}
};
for entry in entries {
let entry = entry.ctx(dir)?;
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
let Some(rest) = name.strip_prefix(&prefix) else {
continue;
};
let Some(first) = rest.strip_suffix(".wal") else {
continue;
};
let Ok(first) = first.parse::<u64>() else {
continue;
};
out.push((entry.path(), first));
}
out.sort_by_key(|(_, first)| *first);
Ok(out)
}
fn roll(&self, inner: &mut Inner) -> Result<()> {
let path = self
.dir
.join(format!("{:08x}-{:020}.wal", self.node, inner.next_seq));
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.ctx(&path)?;
let old_file = std::mem::replace(&mut inner.file, file);
let old_path = std::mem::replace(&mut inner.path, path);
if inner.dirty {
inner.retired.push((old_path, old_file));
inner.dirty = false;
}
inner.written = 0;
Ok(())
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Replayed {
pub replayed: u64,
pub skipped: u64,
pub bytes: u64,
pub torn_segments: u64,
}
#[cfg_attr(test, derive(Debug))]
struct Frame {
signal: Signal,
seq: u64,
body: Vec<u8>,
}
struct FrameReader {
file: File,
path: PathBuf,
done: bool,
}
impl FrameReader {
fn open(path: &Path) -> Result<FrameReader> {
Ok(FrameReader {
file: File::open(path).ctx(path)?,
path: path.to_path_buf(),
done: false,
})
}
fn next_frame(&mut self) -> Result<Option<Frame>> {
let mut header = [0u8; HEADER_LEN];
if !read_exact_or_eof(&mut self.file, &mut header).ctx(&self.path)? {
return Ok(None);
}
let magic = u32::from_le_bytes(header[0..4].try_into().unwrap_or_default());
let version = u16::from_le_bytes(header[4..6].try_into().unwrap_or_default());
let len = u32::from_le_bytes(header[16..20].try_into().unwrap_or_default());
if magic != MAGIC {
return Err(Error::WalCorrupt {
path: self.path.clone(),
why: "bad frame magic",
});
}
if version != WAL_VERSION {
return Err(Error::WalVersion {
path: self.path.clone(),
found: version,
expected: WAL_VERSION,
});
}
if len > MAX_FRAME_BYTES {
return Err(Error::WalCorrupt {
path: self.path.clone(),
why: "frame length above the maximum",
});
}
let Some(signal) = Signal::from_u8(header[6]) else {
return Err(Error::WalCorrupt {
path: self.path.clone(),
why: "unknown signal in frame header",
});
};
let mut body = vec![0u8; len as usize];
if !read_exact_or_eof(&mut self.file, &mut body).ctx(&self.path)? {
return Err(Error::WalCorrupt {
path: self.path.clone(),
why: "truncated frame body",
});
}
let mut crc_bytes = [0u8; CRC_LEN];
if !read_exact_or_eof(&mut self.file, &mut crc_bytes).ctx(&self.path)? {
return Err(Error::WalCorrupt {
path: self.path.clone(),
why: "truncated frame checksum",
});
}
let mut hasher = crc32fast::Hasher::new();
hasher.update(&header);
hasher.update(&body);
if hasher.finalize() != u32::from_le_bytes(crc_bytes) {
return Err(Error::WalCorrupt {
path: self.path.clone(),
why: "frame checksum mismatch",
});
}
Ok(Some(Frame {
signal,
seq: u64::from_le_bytes(header[8..16].try_into().unwrap_or_default()),
body,
}))
}
}
impl Iterator for FrameReader {
type Item = Result<Frame>;
fn next(&mut self) -> Option<Result<Frame>> {
if self.done {
return None;
}
match self.next_frame() {
Ok(Some(frame)) => Some(Ok(frame)),
Ok(None) => {
self.done = true;
None
}
Err(e) => {
self.done = true;
Some(Err(e))
}
}
}
}
fn read_exact_or_eof(file: &mut impl Read, buf: &mut [u8]) -> std::io::Result<bool> {
let mut filled = 0;
while filled < buf.len() {
match file.read(&mut buf[filled..]) {
Ok(0) => return Ok(false),
Ok(n) => filled += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
fn tmpdir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("mira-wal-{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
fn collect(root: &Path, node: u32, wm: Watermarks) -> (Vec<(Signal, Vec<u8>)>, Replayed) {
let (got, _, stats) = collect_seqs(root, node, wm);
(got, stats)
}
#[allow(clippy::type_complexity)]
fn collect_seqs(
root: &Path,
node: u32,
wm: Watermarks,
) -> (Vec<(Signal, Vec<u8>)>, Vec<u64>, Replayed) {
let (mut got, mut seqs) = (Vec::new(), Vec::new());
let stats = Wal::replay(root, node, wm, |s, seq, b| {
got.push((s, b.to_vec()));
seqs.push(seq);
Ok(())
})
.unwrap();
(got, seqs, stats)
}
#[test]
fn a_frame_round_trips_through_replay() {
let root = tmpdir("roundtrip");
let wal = Wal::open(&root, 0xab).unwrap();
assert_eq!(wal.append(Signal::Logs, b"one").unwrap(), 0);
assert_eq!(wal.append(Signal::Traces, b"two").unwrap(), 1);
assert_eq!(wal.append(Signal::Metrics, b"three").unwrap(), 2);
let (got, stats) = collect(&root, 0xab, [0, 0, 0]);
assert_eq!(stats.replayed, 3);
assert_eq!(got[0], (Signal::Logs, b"one".to_vec()));
assert_eq!(got[2], (Signal::Metrics, b"three".to_vec()));
}
#[test]
fn a_published_block_is_not_replayed_and_each_signal_counts_separately() {
let root = tmpdir("watermark");
let wal = Wal::open(&root, 1).unwrap();
wal.append(Signal::Logs, b"l0").unwrap(); wal.append(Signal::Traces, b"t1").unwrap(); wal.append(Signal::Logs, b"l2").unwrap(); wal.append(Signal::Traces, b"t3").unwrap();
let mut wm = [0u64; 3];
wm[Signal::Logs.index()] = 3;
wm[Signal::Traces.index()] = 2;
let (got, stats) = collect(&root, 1, wm);
assert_eq!(stats.replayed, 1);
assert_eq!(stats.skipped, 3);
assert_eq!(got, vec![(Signal::Traces, b"t3".to_vec())]);
}
#[test]
fn a_torn_tail_ends_the_segment_without_losing_what_came_before() {
let root = tmpdir("torn");
let wal = Wal::open(&root, 2).unwrap();
wal.append(Signal::Logs, b"complete").unwrap();
wal.append(Signal::Logs, b"also-complete").unwrap();
wal.sync().unwrap();
let path = {
let inner = wal.inner.lock().unwrap();
inner.path.clone()
};
drop(wal);
let len = fs::metadata(&path).unwrap().len();
OpenOptions::new()
.write(true)
.open(&path)
.unwrap()
.set_len(len - 4)
.unwrap();
let (got, stats) = collect(&root, 2, [0, 0, 0]);
assert_eq!(
stats.replayed, 1,
"the whole frame before the tear survives"
);
assert_eq!(stats.torn_segments, 1);
assert_eq!(got, vec![(Signal::Logs, b"complete".to_vec())]);
}
#[test]
fn a_flipped_bit_in_the_body_is_caught_by_the_checksum() {
let root = tmpdir("bitrot");
let wal = Wal::open(&root, 3).unwrap();
wal.append(Signal::Logs, b"the-quick-brown-fox").unwrap();
wal.sync().unwrap();
let path = {
let inner = wal.inner.lock().unwrap();
inner.path.clone()
};
drop(wal);
let mut bytes = fs::read(&path).unwrap();
bytes[HEADER_LEN + 3] ^= 0x40;
fs::write(&path, &bytes).unwrap();
let (got, stats) = collect(&root, 3, [0, 0, 0]);
assert!(
got.is_empty(),
"a corrupt frame is never handed to the callback"
);
assert_eq!(stats.torn_segments, 1);
}
#[test]
fn a_corrupt_length_is_refused_before_it_is_allocated() {
let root = tmpdir("badlen");
let wal = Wal::open(&root, 4).unwrap();
wal.append(Signal::Logs, b"small").unwrap();
wal.sync().unwrap();
let path = {
let inner = wal.inner.lock().unwrap();
inner.path.clone()
};
drop(wal);
let mut bytes = fs::read(&path).unwrap();
bytes[16..20].copy_from_slice(&u32::MAX.to_le_bytes());
fs::write(&path, &bytes).unwrap();
let mut reader = FrameReader::open(&path).unwrap();
let err = reader.next().unwrap().unwrap_err();
assert!(
matches!(&err, Error::WalCorrupt { why, .. } if why.contains("length")),
"got {err:?}"
);
}
#[test]
fn a_frame_larger_than_the_maximum_is_refused_on_append() {
let root = tmpdir("toobig");
let wal = Wal::open(&root, 5).unwrap();
let huge = vec![0u8; MAX_FRAME_BYTES as usize + 1];
assert!(matches!(
wal.append(Signal::Logs, &huge),
Err(Error::WalFrameTooLarge { .. })
));
}
#[test]
fn the_sequence_resumes_past_everything_on_disk_after_a_restart() {
let root = tmpdir("resume");
let wal = Wal::open(&root, 6).unwrap();
wal.append(Signal::Logs, b"a").unwrap();
wal.append(Signal::Logs, b"b").unwrap();
wal.sync().unwrap();
drop(wal);
let wal = Wal::open(&root, 6).unwrap();
assert_eq!(wal.next_seq(), 2);
assert_eq!(wal.append(Signal::Logs, b"c").unwrap(), 2);
let (got, _) = collect(&root, 6, [0, 0, 0]);
assert_eq!(got.len(), 3);
}
#[test]
fn replay_carries_the_original_sequence_and_append_reports_it_under_the_lock() {
let root = tmpdir("seqs");
let wal = Wal::open(&root, 3).unwrap();
let mut seen = Vec::new();
for body in [b"a", b"b", b"c"] {
wal.append_then(Signal::Traces, body, |seq| seen.push(seq))
.unwrap();
}
assert_eq!(seen, [0, 1, 2]);
let mut wm = [0u64; 3];
wm[Signal::Traces.index()] = 1;
let (got, seqs, stats) = collect_seqs(&root, 3, wm);
assert_eq!(seqs, [1, 2]);
assert_eq!(got.len(), 2);
assert_eq!(stats.skipped, 1);
}
#[test]
fn another_nodes_segments_are_left_alone() {
let root = tmpdir("twonodes");
let a = Wal::open(&root, 0x11).unwrap();
let b = Wal::open(&root, 0x22).unwrap();
a.append(Signal::Logs, b"from-a").unwrap();
b.append(Signal::Logs, b"from-b").unwrap();
a.sync().unwrap();
b.sync().unwrap();
let (got, _) = collect(&root, 0x11, [0, 0, 0]);
assert_eq!(got, vec![(Signal::Logs, b"from-a".to_vec())]);
}
#[test]
fn truncate_removes_covered_segments_and_never_the_open_one() {
let root = tmpdir("truncate");
let wal = Wal::open(&root, 7).unwrap();
wal.append(Signal::Logs, b"old").unwrap();
{
let mut inner = wal.inner.lock().unwrap();
inner.written = SEGMENT_BYTES;
}
wal.append(Signal::Logs, b"new").unwrap();
assert_eq!(Wal::segments(&wal.dir, 7).unwrap().len(), 2);
assert_eq!(wal.truncate(1).unwrap(), 1);
let segments = Wal::segments(&wal.dir, 7).unwrap();
assert_eq!(segments.len(), 1, "the open segment is never unlinked");
let (got, _) = collect(&root, 7, [0, 0, 0]);
assert_eq!(got, vec![(Signal::Logs, b"new".to_vec())]);
}
#[test]
fn truncate_keeps_a_segment_whose_tail_is_still_uncovered() {
let root = tmpdir("truncate-partial");
let wal = Wal::open(&root, 8).unwrap();
wal.append(Signal::Logs, b"covered").unwrap(); wal.append(Signal::Logs, b"not-yet").unwrap(); {
let mut inner = wal.inner.lock().unwrap();
inner.written = SEGMENT_BYTES;
}
wal.append(Signal::Logs, b"newest").unwrap();
assert_eq!(wal.truncate(1).unwrap(), 0);
assert_eq!(wal.truncate(2).unwrap(), 1);
}
#[test]
fn a_wrong_frame_header_is_named_by_what_is_wrong_with_it() {
let root = tmpdir("corrupt-frames");
let wal = Wal::open(&root, 0x11).unwrap();
wal.append(Signal::Logs, b"body").unwrap();
wal.sync().unwrap();
let good = fs::read(root.join(".wal").join("00000011-00000000000000000000.wal")).unwrap();
assert_eq!(good.len(), HEADER_LEN + 4 + CRC_LEN);
let broken = |f: &dyn Fn(&mut Vec<u8>)| -> Error {
let mut bytes = good.clone();
f(&mut bytes);
let path = root.join("mangled.wal");
fs::write(&path, &bytes).unwrap();
let mut reader = FrameReader::open(&path).unwrap();
let e = reader.next().unwrap().unwrap_err();
assert!(reader.next().is_none());
e
};
let corrupt = |f: &dyn Fn(&mut Vec<u8>), want: &str| {
let e = broken(f);
assert!(
matches!(&e, Error::WalCorrupt { why, .. } if *why == want),
"expected {want:?}, got {e}"
);
};
corrupt(&|b| b[0] ^= 0xff, "bad frame magic");
corrupt(&|b| b[6] = 0xfe, "unknown signal in frame header");
corrupt(&|b| b.truncate(HEADER_LEN + 1), "truncated frame body");
let e = broken(&|b| b[4..6].copy_from_slice(&(WAL_VERSION + 7).to_le_bytes()));
assert!(
matches!(&e, Error::WalVersion { found, expected, .. }
if (*found, *expected) == (WAL_VERSION + 7, WAL_VERSION)),
"expected a version error naming both sides, got {e}"
);
}
#[test]
fn replaying_an_absent_directory_is_not_an_error() {
let root = tmpdir("empty");
let (got, stats) = collect(&root, 9, [0, 0, 0]);
assert!(got.is_empty());
assert_eq!(stats, Replayed::default());
}
fn torn_frame_bytes(seq: u64) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&MAGIC.to_le_bytes());
bytes.extend_from_slice(&WAL_VERSION.to_le_bytes());
bytes.push(Signal::Logs as u8);
bytes.push(0); bytes.extend_from_slice(&seq.to_le_bytes());
bytes.extend_from_slice(&64u32.to_le_bytes()); assert_eq!(bytes.len(), HEADER_LEN);
bytes.extend_from_slice(b"only-a-few"); bytes
}
#[test]
fn a_torn_tail_does_not_stop_the_log_opening_or_get_appended_behind() {
let root = tmpdir("open-torn-tail");
let wal = Wal::open(&root, 0x4a).unwrap();
wal.append(Signal::Logs, b"kept").unwrap();
wal.append(Signal::Logs, b"in-flight").unwrap();
wal.sync().unwrap();
let path = {
let inner = wal.inner.lock().unwrap();
inner.path.clone()
};
drop(wal);
let len = fs::metadata(&path).unwrap().len();
OpenOptions::new()
.write(true)
.open(&path)
.unwrap()
.set_len(len - 4)
.unwrap();
let wal = Wal::open(&root, 0x4a).expect("a torn tail is a boot condition, not an error");
assert_eq!(wal.append(Signal::Logs, b"after").unwrap(), 1);
wal.sync().unwrap();
let (got, stats) = collect(&root, 0x4a, [0, 0, 0]);
assert_eq!(
got,
vec![
(Signal::Logs, b"kept".to_vec()),
(Signal::Logs, b"after".to_vec())
],
"the frame before the tear and the one after the restart both replay"
);
assert_eq!(stats.torn_segments, 1);
let root = tmpdir("open-torn-head");
let wal = Wal::open(&root, 0x4b).unwrap();
wal.append(Signal::Logs, b"kept").unwrap(); wal.sync().unwrap();
drop(wal);
let head = root
.join(".wal")
.join(format!("{:08x}-{:020}.wal", 0x4b, 1));
fs::write(&head, torn_frame_bytes(1)).unwrap();
let wal = Wal::open(&root, 0x4b).unwrap();
assert_eq!(
wal.next_seq(),
2,
"the torn segment's own name is not reused"
);
wal.append(Signal::Logs, b"after").unwrap();
wal.sync().unwrap();
let (got, stats) = collect(&root, 0x4b, [0, 0, 0]);
assert_eq!(
got,
vec![
(Signal::Logs, b"kept".to_vec()),
(Signal::Logs, b"after".to_vec())
],
"a frame acked after the restart is still replayable"
);
assert_eq!(stats.torn_segments, 1, "the torn segment is still counted");
}
#[test]
fn the_next_sync_forces_a_segment_the_appender_rolled_away() {
let root = tmpdir("retired");
let wal = Wal::open(&root, 0x5a).unwrap();
wal.append(Signal::Logs, b"in the outgoing segment")
.unwrap();
{
let mut inner = wal.inner.lock().unwrap();
wal.roll(&mut inner).unwrap();
assert_eq!(inner.retired.len(), 1, "queued for the timer, not forced");
assert!(!inner.dirty, "and the new segment has nothing in it");
}
wal.sync().unwrap();
{
let inner = wal.inner.lock().unwrap();
assert!(
inner.retired.is_empty(),
"taken, so a later tick does not pay for the same barrier again"
);
}
wal.sync().unwrap();
let (got, _) = collect(&root, 0x5a, [0, 0, 0]);
assert_eq!(
got,
vec![(Signal::Logs, b"in the outgoing segment".to_vec())],
"rolling is not a truncation"
);
}
#[test]
fn truncate_drops_the_empty_segment_a_crash_left_behind() {
let root = tmpdir("empty-segment");
let wal = Wal::open(&root, 0x6b).unwrap();
wal.append(Signal::Logs, b"live").unwrap();
let stale = root
.join(".wal")
.join(format!("{:08x}-{:020}.wal", 0x6b, 7));
File::create(&stale).unwrap();
assert_eq!(Wal::segments(&wal.dir, 0x6b).unwrap().len(), 2);
assert_eq!(wal.truncate(0).unwrap(), 1);
assert!(!stale.exists());
let (got, _) = collect(&root, 0x6b, [0, 0, 0]);
assert_eq!(
got,
vec![(Signal::Logs, b"live".to_vec())],
"the open segment is untouched"
);
}
#[test]
fn segments_lists_only_this_nodes_well_formed_segments() {
let root = tmpdir("listing");
let wal = Wal::open(&root, 0x7c).unwrap();
wal.append(Signal::Logs, b"real").unwrap();
let dir = root.join(".wal");
for junk in [
"0000007c-00000000000000000009.log", "0000007c-not-a-number.wal", "0000007c-.wal", "readme.txt", "0000007d-00000000000000000000.wal", ] {
File::create(dir.join(junk)).unwrap();
}
let listed = Wal::segments(&dir, 0x7c).unwrap();
assert_eq!(listed.len(), 1, "only the real segment, got {listed:?}");
assert_eq!(listed[0].1, 0);
assert!(
Wal::segments(&root.join("never-created"), 0x7c)
.unwrap()
.is_empty()
);
let notdir = root.join("a-file-not-a-dir");
fs::write(¬dir, b"x").unwrap();
assert!(matches!(
Wal::segments(¬dir, 0x7c),
Err(Error::Io { .. })
));
}
#[test]
fn a_failed_read_is_never_mistaken_for_the_end_of_a_segment() {
let root = tmpdir("read-error");
let notafile = root.join("a-directory");
fs::create_dir_all(¬afile).unwrap();
let mut reader =
FrameReader::open(¬afile).expect("opening a directory is not itself the failure");
let err = reader
.next_frame()
.expect_err("a failed read is an error, not an end of segment");
match &err {
Error::Io { path, source } => {
assert_eq!(path, ¬afile, "the error names the segment: {err}");
assert!(
source.raw_os_error().is_some(),
"the errno is carried through rather than synthesised: {err}"
);
}
other => panic!("a failing read is an io error, got {other}"),
}
let mut reader = FrameReader::open(¬afile).unwrap();
assert!(
matches!(reader.next(), Some(Err(Error::Io { .. }))),
"the failure is yielded, not swallowed into an end of segment"
);
assert!(reader.next().is_none(), "and the reader is spent after it");
let empty = root.join("empty.wal");
File::create(&empty).unwrap();
assert!(
FrameReader::open(&empty)
.unwrap()
.next_frame()
.unwrap()
.is_none(),
"an end of file is `Ok(None)`, and only a tear is an `Err`"
);
}
#[test]
fn signal_bytes_match_the_block_directory_names() {
assert_eq!(Signal::Logs.as_str(), "logs");
assert_eq!(Signal::Traces.as_str(), "traces");
assert_eq!(Signal::Metrics.as_str(), "metrics");
for s in Signal::ALL {
assert_eq!(Signal::from_u8(s as u8), Some(s));
}
assert_eq!(Signal::from_u8(3), None);
}
}