driver-manager: N20 audit - log silent heartbeat write failures (S6)

Replace three silently-dropped Result cases in heartbeat::write_atomic
(create_dir_all, fs::write, fs::rename) with log::error! diagnostics.
Brings heartbeat.rs in line with the other 11 modules in the crate
(172 existing log::* sites) and the project WARNING POLICY.

Also removes the orphaned temp file when rename fails (e.g. cross-fs
EXDEV) so leftover .tmp files do not accumulate across restarts.

Success-path behavior is unchanged; the 3 existing heartbeat unit
tests still pass.
This commit is contained in:
2026-07-28 06:24:58 +09:00
parent f7fd8f8eee
commit ad2a8ee15d
@@ -98,14 +98,43 @@ impl Heartbeat {
fn write_atomic(path: &Path, bytes: &[u8]) {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
let _ = fs::create_dir_all(parent);
// N20 S6: log create_dir_all failure (was silently dropped).
if let Err(e) = fs::create_dir_all(parent) {
log::error!(
"heartbeat: create_dir_all({}) failed: {}",
parent.display(),
e
);
return;
}
}
}
let tmp = path.with_extension("tmp");
if fs::write(&tmp, bytes).is_err() {
// N20 S6: log write failure (was silently dropped). Includes
// disk-full, read-only-fs, permission-denied. Heartbeat-thread
// continues running (heartbeat is best-effort) but operators now
// see the actual cause in the log.
if let Err(e) = fs::write(&tmp, bytes) {
log::error!(
"heartbeat: write {} ({} bytes) failed: {}",
tmp.display(),
bytes.len(),
e
);
return;
}
let _ = fs::rename(&tmp, path);
// N20 S6: log rename failure (was silently dropped). Cross-fs
// rename (e.g. tmpfs → /var/run on different fs) returns EXDEV.
// The temp file is left behind but a subsequent write will overwrite.
if let Err(e) = fs::rename(&tmp, path) {
log::error!(
"heartbeat: rename {} -> {} failed: {}",
tmp.display(),
path.display(),
e
);
let _ = fs::remove_file(&tmp);
}
}
/// Cheap cloneable handle that the manager uses to bump counters.