brokerd: recover a torn line in place, real dates only, bounded ttl, EMFILE
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>
This commit is contained in:
+38
-21
@@ -191,12 +191,35 @@ fn tail_of_file_before_latest(dir: &Path) -> Result<Hash32, AuditError> {
|
||||
}
|
||||
}
|
||||
|
||||
/// End the last line of the log with a newline, in the file that holds it: the last log file that
|
||||
/// is not empty. A torn line is always there, even when an empty later file exists, and it must be
|
||||
/// ended in place, or the next record would join it on one line.
|
||||
fn end_last_line(dir: &Path) -> Result<(), AuditError> {
|
||||
for name in log_files(dir)?.iter().rev() {
|
||||
let path = dir.join(name);
|
||||
let bytes = fs::read(&path).map_err(|e| io("read audit log", &path, e))?;
|
||||
match bytes.last() {
|
||||
None => continue,
|
||||
Some(b'\n') => return Ok(()),
|
||||
Some(_) => {
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|e| io("open audit log", &path, e))?;
|
||||
file.write_all(b"\n").map_err(io::Error::other)?;
|
||||
file.sync_all().map_err(io::Error::other)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Append one record: serialise it, open the target file, write the line, and return the seq and
|
||||
/// the hash of the line it wrote. A fresh file is created mode 0600 and the directory synced.
|
||||
fn write_record(
|
||||
dir: &Path,
|
||||
target: &str,
|
||||
newline_first: bool,
|
||||
seq: u64,
|
||||
prev: Hash32,
|
||||
event: AuditEvent,
|
||||
@@ -216,9 +239,6 @@ fn write_record(
|
||||
.mode(0o600)
|
||||
.open(&path)
|
||||
.map_err(|e| io("open audit log", &path, e))?;
|
||||
if newline_first {
|
||||
file.write_all(b"\n").map_err(io::Error::other)?;
|
||||
}
|
||||
file.write_all(json.as_bytes()).map_err(io::Error::other)?;
|
||||
file.write_all(b"\n").map_err(io::Error::other)?;
|
||||
file.sync_all().map_err(io::Error::other)?;
|
||||
@@ -305,10 +325,12 @@ impl Writer {
|
||||
if !accept_break {
|
||||
return Err(AuditError::Broken(Box::new(failure.clone())));
|
||||
}
|
||||
if failure.tail_torn {
|
||||
end_last_line(dir)?;
|
||||
}
|
||||
let (seq, hash) = write_record(
|
||||
dir,
|
||||
latest.as_deref().unwrap_or(&day_name(now)),
|
||||
failure.tail_torn,
|
||||
failure.break_seq,
|
||||
failure.break_prev,
|
||||
AuditEvent::AcceptedBreak {
|
||||
@@ -322,10 +344,12 @@ impl Writer {
|
||||
} else if accept_break {
|
||||
return Err(AuditError::NothingToAccept);
|
||||
} else if let Some(torn) = report.torn_tail.as_ref() {
|
||||
if !torn.has_newline {
|
||||
end_last_line(dir)?;
|
||||
}
|
||||
let (seq, hash) = write_record(
|
||||
dir,
|
||||
latest.as_deref().unwrap_or(&day_name(now)),
|
||||
!torn.has_newline,
|
||||
torn.recovery_seq,
|
||||
torn.recovery_prev,
|
||||
AuditEvent::Recovery {
|
||||
@@ -366,21 +390,14 @@ impl Writer {
|
||||
return Err(AuditError::Stopped);
|
||||
}
|
||||
let target = target_for(&self.latest, time);
|
||||
let (seq, hash) = match write_record(
|
||||
&self.path,
|
||||
&target,
|
||||
false,
|
||||
self.next_seq,
|
||||
self.prev,
|
||||
event,
|
||||
time,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
self.stopped = true;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let (seq, hash) =
|
||||
match write_record(&self.path, &target, self.next_seq, self.prev, event, time) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
self.stopped = true;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
self.prev = hash;
|
||||
self.next_seq = seq.checked_add(1).unwrap_or(self.next_seq);
|
||||
if self.latest.as_deref() != Some(target.as_str()) {
|
||||
|
||||
Reference in New Issue
Block a user