disk_watch: stay silent when the agent owns nothing worth deleting
The todo fired on filesystem usage alone, so every agent on a busy host got woken by a disk it had no power over. Measured case: a 927G volume at 81%, 742G used, of which one agent's entire tree was 5.3G. The todo cost that agent a full turn to arrive at "not actionable". `summary_for` now also requires a non-empty `big_dirs()` result. An agent that owns no oversized directory cannot free meaningful space, so there is nothing to say to it; the shared store filling up is the host's signal. Agents that ARE sitting on a stale `target/` still get told, with the same bucketed anti-nag summary as before. Drops the conditional around the "Biggest directories" section, which is now unreachable when empty. Existing tests passed `&[]` as a don't-care — they now pass a directory, since that argument became load-bearing. Closes #2759
This commit is contained in:
parent
f28a1e33d3
commit
c3e6e23d22
1 changed files with 29 additions and 17 deletions
|
|
@ -115,8 +115,14 @@ fn probe() -> Option<String> {
|
||||||
/// percentage + set of oversized directories. Separated so the threshold
|
/// percentage + set of oversized directories. Separated so the threshold
|
||||||
/// and — more importantly — the *stability* of the summary are unit-tested
|
/// and — more importantly — the *stability* of the summary are unit-tested
|
||||||
/// without a real filesystem.
|
/// without a real filesystem.
|
||||||
|
///
|
||||||
|
/// Silent unless the agent owns something worth deleting. A full disk the
|
||||||
|
/// agent did not fill is not its problem to solve: it cannot free host
|
||||||
|
/// bytes, so the todo would only ever cost a turn to conclude "not
|
||||||
|
/// actionable". The shared store filling up is the host's signal, not an
|
||||||
|
/// agent's.
|
||||||
fn summary_for(pct: u64, dirs: &[PathBuf]) -> Option<String> {
|
fn summary_for(pct: u64, dirs: &[PathBuf]) -> Option<String> {
|
||||||
if pct < WARN_PCT {
|
if pct < WARN_PCT || dirs.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let bucket = pct / BUCKET_PCT * BUCKET_PCT;
|
let bucket = pct / BUCKET_PCT * BUCKET_PCT;
|
||||||
|
|
@ -125,16 +131,12 @@ fn summary_for(pct: u64, dirs: &[PathBuf]) -> Option<String> {
|
||||||
Only delete things that are actually big (tens of GiB); a few MB of notes won't move the needle.\n\
|
Only delete things that are actually big (tens of GiB); a few MB of notes won't move the needle.\n\
|
||||||
First candidates: regenerable build output in your workspace (`target/`, `node_modules/`, `dist/`).\n\
|
First candidates: regenerable build output in your workspace (`target/`, `node_modules/`, `dist/`).\n\
|
||||||
Never delete anything still needed. If nothing is safe to drop, tell the operator you need more space \
|
Never delete anything still needed. If nothing is safe to drop, tell the operator you need more space \
|
||||||
rather than forcing it — be cautious with deletions in general."
|
rather than forcing it — be cautious with deletions in general.\n\
|
||||||
|
Biggest directories under your own tree (`du -sh` them before removing anything):"
|
||||||
);
|
);
|
||||||
if !dirs.is_empty() {
|
for dir in dirs {
|
||||||
out.push_str(
|
// Infallible: writing into a String.
|
||||||
"\nBiggest directories under your own tree (`du -sh` them before removing anything):",
|
let _ = write!(out, "\n - {}", dir.display());
|
||||||
);
|
|
||||||
for dir in dirs {
|
|
||||||
// Infallible: writing into a String.
|
|
||||||
let _ = write!(out, "\n - {}", dir.display());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
@ -273,15 +275,23 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn under_threshold_is_silent() {
|
fn under_threshold_is_silent() {
|
||||||
assert!(summary_for(WARN_PCT - 1, &[]).is_none());
|
assert!(summary_for(WARN_PCT - 1, &[p("/agents/a/state/hh/target")]).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_threshold_speaks_up() {
|
fn at_threshold_speaks_up() {
|
||||||
let s = summary_for(WARN_PCT, &[]).expect("todo");
|
let s = summary_for(WARN_PCT, &[p("/agents/a/state/hh/target")]).expect("todo");
|
||||||
assert!(s.contains("over 80% full"));
|
assert!(s.contains("over 80% full"));
|
||||||
assert!(s.contains("tens of GiB"));
|
assert!(s.contains("tens of GiB"));
|
||||||
assert!(s.contains("tell the operator"));
|
assert!(s.contains("tell the operator"));
|
||||||
|
assert!(s.contains("/agents/a/state/hh/target"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A full disk the agent did not fill is not its problem: it cannot
|
||||||
|
/// free host bytes, so naming no directories means saying nothing.
|
||||||
|
#[test]
|
||||||
|
fn full_disk_with_nothing_big_of_ours_is_silent() {
|
||||||
|
assert!(summary_for(99, &[]).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The anti-nag property: usage drifting inside one bucket must render
|
/// The anti-nag property: usage drifting inside one bucket must render
|
||||||
|
|
@ -289,15 +299,17 @@ mod tests {
|
||||||
/// nothing wakes the agent.
|
/// nothing wakes the agent.
|
||||||
#[test]
|
#[test]
|
||||||
fn drift_inside_a_bucket_is_identical() {
|
fn drift_inside_a_bucket_is_identical() {
|
||||||
let a = summary_for(85, &[]).expect("todo");
|
let dirs = [p("/agents/a/state/hh/target")];
|
||||||
let b = summary_for(89, &[]).expect("todo");
|
let a = summary_for(85, &dirs).expect("todo");
|
||||||
|
let b = summary_for(89, &dirs).expect("todo");
|
||||||
assert_eq!(a, b);
|
assert_eq!(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn crossing_a_bucket_changes_the_summary() {
|
fn crossing_a_bucket_changes_the_summary() {
|
||||||
let a = summary_for(89, &[]).expect("todo");
|
let dirs = [p("/agents/a/state/hh/target")];
|
||||||
let b = summary_for(90, &[]).expect("todo");
|
let a = summary_for(89, &dirs).expect("todo");
|
||||||
|
let b = summary_for(90, &dirs).expect("todo");
|
||||||
assert_ne!(a, b);
|
assert_ne!(a, b);
|
||||||
assert!(b.contains("over 90% full"));
|
assert!(b.contains("over 90% full"));
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +365,7 @@ mod tests {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
let todos = Todos::open(&dir.path().join("state.sqlite")).expect("open");
|
let todos = Todos::open(&dir.path().join("state.sqlite")).expect("open");
|
||||||
let wake = Notify::new();
|
let wake = Notify::new();
|
||||||
let summary = summary_for(90, &[]).expect("todo");
|
let summary = summary_for(90, &[p("/agents/a/state/hh/target")]).expect("todo");
|
||||||
|
|
||||||
reconcile(&todos, &wake, Some(&summary));
|
reconcile(&todos, &wake, Some(&summary));
|
||||||
assert_eq!(todos.list(Some(SUBSYSTEM)).expect("list").len(), 1);
|
assert_eq!(todos.list(Some(SUBSYSTEM)).expect("list").len(), 1);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue