Audit writer: open a record-less log, share the log-name rule, keep the lock file

M3a review findings 1, 2, 4 and part of 5. One empty log file made brokerd
panic at startup (files[len - 2]); it now opens as an empty log. brokerd's
name check tested one month digit, so a file bxctl ignored could become
brokerd's latest file; both now use proto::is_audit_log_name. Writer no
longer unlinks audit/.lock, which opened a two-writer window. No unwrap in
short_check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 21:05:13 -07:00
co-authored by Claude Opus 5.5
parent 22a26aad45
commit eed0a221ca
6 changed files with 182 additions and 70 deletions
+106
View File
@@ -0,0 +1,106 @@
//! Audit writer edge cases found in the M3a review: a log with no complete record, a file whose
//! name only looks like a log, and the lock file outliving the writer.
#[path = "support/audit_dir.rs"]
mod audit_dir;
use audit_dir::{D1, D2, TempDir, denied, lines, ts};
use brokerd::audit::{Writer, verify_dir};
use proto::{AuditRecord, Hash32};
/// A kill between creating the day's file and writing its first record leaves it empty. It must
/// open like an empty log, not panic.
#[test]
fn one_zero_length_log_file_opens_as_an_empty_log() {
let dir = TempDir::unmade("zero-length");
std::fs::create_dir_all(&dir.path).unwrap();
std::fs::write(dir.path.join(D1), b"").unwrap();
let opened = Writer::open(&dir.path, false).unwrap();
assert!(!opened.recovered);
let mut writer = opened.writer;
assert_eq!(writer.next_seq(), 0);
writer
.append(ts("2026-09-17T08:00:00.000Z"), denied(1))
.unwrap();
let record: AuditRecord = serde_json::from_str(&lines(&dir.path, D1)[0]).unwrap();
assert_eq!((record.seq, record.prev), (0, Hash32::ZERO));
let report = verify_dir(&dir.path).unwrap();
assert_eq!(report.failure, None);
assert_eq!(report.records, 1);
}
/// The same crash, a day later: the older file holds records and the new one is empty.
#[test]
fn a_zero_length_file_after_records_continues_the_chain() {
let dir = TempDir::unmade("zero-length-later");
let mut writer = Writer::open(&dir.path, false).unwrap().writer;
writer
.append(ts("2026-09-17T08:00:00.000Z"), denied(1))
.unwrap();
drop(writer);
std::fs::write(dir.path.join(D2), b"").unwrap();
let mut writer = Writer::open(&dir.path, false).unwrap().writer;
assert_eq!(writer.next_seq(), 1);
writer
.append(ts("2026-09-18T08:00:00.000Z"), denied(2))
.unwrap();
let report = verify_dir(&dir.path).unwrap();
assert_eq!(report.failure, None);
assert_eq!(report.records, 2);
}
/// A crash part-way through the very first record: one file, one torn line, nothing else.
#[test]
fn a_single_torn_first_record_is_recovered() {
let dir = TempDir::unmade("torn-only");
std::fs::create_dir_all(&dir.path).unwrap();
std::fs::write(dir.path.join(D1), br#"{"seq":0,"time":"2026-09-17T08:"#).unwrap();
let opened = Writer::open(&dir.path, false).unwrap();
assert!(opened.recovered);
let report = verify_dir(&dir.path).unwrap();
assert_eq!(report.failure, None);
assert_eq!(report.recoveries.len(), 1);
}
/// `2026-0x-19.jsonl` is not a log file, for `brokerd` as for `bxctl audit verify`: it is neither
/// verified nor written to, and the real log goes on in its own files.
#[test]
fn a_file_that_only_looks_like_a_log_is_not_part_of_it() {
let dir = TempDir::case("good", None);
let odd = dir.path.join("2026-0x-19.jsonl");
std::fs::write(&odd, b"not a record\n").unwrap();
let mut writer = Writer::open(&dir.path, false)
.expect("a stray file does not break the chain")
.writer;
writer
.append(ts("2026-09-18T09:00:00.000Z"), denied(9))
.unwrap();
assert_eq!(
std::fs::read(&odd).unwrap(),
b"not a record\n",
"never written"
);
let report = verify_dir(&dir.path).unwrap();
assert_eq!(report.failure, None);
assert_eq!(
report.records, 11,
"the ten fixture records and the new one"
);
}
/// The lock file is never removed: a `brokerd` holding the old file open could otherwise lock it
/// while a new one creates and locks a fresh file, and both would write.
#[test]
fn the_lock_file_outlives_the_writer() {
let dir = TempDir::unmade("lock-stays");
let writer = Writer::open(&dir.path, false).unwrap().writer;
drop(writer);
assert!(dir.path.join(".lock").exists());
Writer::open(&dir.path, false).expect("the lock was released with the writer");
}