From the independent review of task 23. A torn last line followed by an empty later file had its recovery written into the later file, which broke the chain for good; the line is now ended in its own file. The log-name rule takes months 01 to 12 and days 01 to 31 only. [approvals] ttl_ms is limited to a day, the longest loopd waits after a pending frame. Running out of file descriptors or memory pauses the listener instead of stopping brokerd (the errors the previous fix skipped do not occur on Linux). args.rs's doc fixed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
135 lines
5.1 KiB
Rust
135 lines
5.1 KiB
Rust
//! 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");
|
|
}
|
|
|
|
/// A torn last line in one file, then an empty later file (created, never written). The torn line
|
|
/// is ended in its own file and the recovery goes on the chain after it, so the log verifies and
|
|
/// the next start is an ordinary one. Found by the independent review of task 23.
|
|
#[test]
|
|
fn a_torn_line_before_an_empty_later_file_is_recovered_in_place() {
|
|
let dir = TempDir::unmade("torn-then-empty");
|
|
let mut writer = Writer::open(&dir.path, false).unwrap().writer;
|
|
writer
|
|
.append(ts("2026-09-17T08:00:00.000Z"), denied(1))
|
|
.unwrap();
|
|
drop(writer);
|
|
let mut d1 = std::fs::read(dir.path.join(D1)).unwrap();
|
|
d1.extend_from_slice(br#"{"seq":1,"time":"2026-09-17T09"#);
|
|
std::fs::write(dir.path.join(D1), &d1).unwrap();
|
|
std::fs::write(dir.path.join(D2), b"").unwrap();
|
|
|
|
let opened = Writer::open(&dir.path, false).unwrap();
|
|
assert!(opened.recovered);
|
|
drop(opened);
|
|
let report = verify_dir(&dir.path).unwrap();
|
|
assert_eq!(report.failure, None, "{:?}", report.failure);
|
|
assert_eq!(report.recoveries.len(), 1);
|
|
assert!(std::fs::read(dir.path.join(D1)).unwrap().ends_with(b"\n"));
|
|
|
|
let again = Writer::open(&dir.path, false).expect("the next start is an ordinary one");
|
|
assert!(!again.recovered);
|
|
}
|