chore: reap docker events, cut per-cycle work, use mkswap --file [P1/P2]
1. docker events was leaking zombies. docker_events_loop spawned the child, read its stdout to EOF and returned without calling wait(). Dropping a Child does not reap it, so every reconnect - which happens whenever Docker restarts or the stream ends - left a defunct process behind for the lifetime of the daemon. The reader loop moved into its own function so the wait happens on every exit path, and the exit status is logged rather than discarded. 2. sysctl status still decided ownership with contains(). The destructive paths were fixed earlier, but inspect_sysctl_tuning kept first.contains(MANAGED_MARKER) in the disabled branch, so a file whose first line merely embedded the marker was reported as managed. Diagnostic-only, but it contradicted the rule the rest of the module follows. Now an exact match on the trimmed first line. 3. Swap was reconciled at the traffic interval. ensure_swap ran every reconcile_interval_secs, default 15, and each run spawns swapon --show, sysctl reads, the persistence check and the final verify. A swapfile, its fstab entry and vm.swappiness change when an operator changes them and at no other time, so this was process spawning for state that does not drift. Swap now runs on its own 300-second schedule; traffic and firewall keep the configured interval. The startup reconcile is unchanged, so a fresh daemon still converges immediately. 4. The event log recorded routine success. Every successful cycle appended a line, and appending means reading the whole file, rewriting it and truncating. At 15 seconds that is roughly 17k entries a day, and the entries worth reading were buried among them. Only transitions are written now: a new failure, a failure whose message changed, and the recovery after one. set_ok and set_err report whether the state actually changed, which is what makes that possible. journald still has the full per-cycle picture through the tracing output. 5. Swapfile creation is one command. mkswap --file <path> --size <N>M creates and formats the file, replacing the fallocate → dd fallback → chmod → mkswap chain: four processes, three failure branches, and a window where the file existed but was not yet formatted. util-linux on Debian 13 also handles filesystem-specific requirements - Btrfs in particular - that raw fallocate does not. fallocate, dd and chmod drop out of the required host tools when swap is enabled. cargo test --workspace: 183 + 4 passed. clippy -D warnings clean.
This commit is contained in:
+20
-35
@@ -1020,12 +1020,27 @@ fn suffixed_path(path: &Path, suffix: &str) -> anyhow::Result<PathBuf> {
|
||||
Ok(path.with_file_name(out))
|
||||
}
|
||||
|
||||
/// Allocates a swapfile and formats it, with the restrictive mode applied before `mkswap`
|
||||
/// so the file is never briefly world-readable while it already holds swap structure.
|
||||
/// Allocates a swapfile and formats it in one step.
|
||||
///
|
||||
/// `mkswap --file --size` creates the file itself, which replaces the previous
|
||||
/// `fallocate`, fall back to `dd`, `chmod`, `mkswap` chain: four processes, three failure
|
||||
/// branches and a window where the file existed but was not yet formatted. util-linux in
|
||||
/// Debian 13 also handles filesystem-specific requirements - notably Btrfs - that raw
|
||||
/// `fallocate` does not.
|
||||
///
|
||||
/// The mode is set again afterwards because the file must be 0600 before it is activated,
|
||||
/// and this way it never sits world-readable while already holding swap structure.
|
||||
fn build_swapfile(path: &Path, size_bytes: u64) -> anyhow::Result<()> {
|
||||
create_swapfile(path, size_bytes)?;
|
||||
set_swapfile_mode(path)?;
|
||||
run("mkswap", &[path.to_string_lossy().as_ref()])?;
|
||||
let size_mb = size_bytes / (1024 * 1024);
|
||||
run(
|
||||
"mkswap",
|
||||
&[
|
||||
"--file",
|
||||
path.to_string_lossy().as_ref(),
|
||||
"--size",
|
||||
&format!("{size_mb}M"),
|
||||
],
|
||||
)?;
|
||||
set_swapfile_mode(path)
|
||||
}
|
||||
|
||||
@@ -1040,36 +1055,6 @@ fn set_swapfile_mode(_path: &Path) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_swapfile(path: &Path, size_bytes: u64) -> anyhow::Result<()> {
|
||||
let size_mb = size_bytes / (1024 * 1024);
|
||||
let p = path.to_string_lossy().to_string();
|
||||
|
||||
let out = Command::new("fallocate")
|
||||
.args(["-l", &format!("{}M", size_mb), &p])
|
||||
.output()?;
|
||||
if out.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let out_dd = Command::new("dd")
|
||||
.args([
|
||||
"if=/dev/zero",
|
||||
&format!("of={p}"),
|
||||
"bs=1M",
|
||||
&format!("count={size_mb}"),
|
||||
"status=none",
|
||||
])
|
||||
.output()?;
|
||||
if !out_dd.status.success() {
|
||||
anyhow::bail!(
|
||||
"swapfile create failed (fallocate+dd): fallocate={}, dd={}",
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
String::from_utf8_lossy(&out_dd.stderr)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rewrite_fstab_exclusive(desired: &DesiredSwap) -> anyhow::Result<()> {
|
||||
let tx = FstabTransaction::begin(Path::new(FSTAB_PATH))?;
|
||||
let rewritten = rewrite_fstab_content(tx.original(), &desired.path, desired.priority);
|
||||
|
||||
Reference in New Issue
Block a user