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
|
||||
/// and — more importantly — the *stability* of the summary are unit-tested
|
||||
/// 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> {
|
||||
if pct < WARN_PCT {
|
||||
if pct < WARN_PCT || dirs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
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\
|
||||
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 \
|
||||
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() {
|
||||
out.push_str(
|
||||
"\nBiggest directories under your own tree (`du -sh` them before removing anything):",
|
||||
);
|
||||
for dir in dirs {
|
||||
// Infallible: writing into a String.
|
||||
let _ = write!(out, "\n - {}", dir.display());
|
||||
}
|
||||
for dir in dirs {
|
||||
// Infallible: writing into a String.
|
||||
let _ = write!(out, "\n - {}", dir.display());
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
|
@ -273,15 +275,23 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
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]
|
||||
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("tens of GiB"));
|
||||
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
|
||||
|
|
@ -289,15 +299,17 @@ mod tests {
|
|||
/// nothing wakes the agent.
|
||||
#[test]
|
||||
fn drift_inside_a_bucket_is_identical() {
|
||||
let a = summary_for(85, &[]).expect("todo");
|
||||
let b = summary_for(89, &[]).expect("todo");
|
||||
let dirs = [p("/agents/a/state/hh/target")];
|
||||
let a = summary_for(85, &dirs).expect("todo");
|
||||
let b = summary_for(89, &dirs).expect("todo");
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crossing_a_bucket_changes_the_summary() {
|
||||
let a = summary_for(89, &[]).expect("todo");
|
||||
let b = summary_for(90, &[]).expect("todo");
|
||||
let dirs = [p("/agents/a/state/hh/target")];
|
||||
let a = summary_for(89, &dirs).expect("todo");
|
||||
let b = summary_for(90, &dirs).expect("todo");
|
||||
assert_ne!(a, b);
|
||||
assert!(b.contains("over 90% full"));
|
||||
}
|
||||
|
|
@ -353,7 +365,7 @@ mod tests {
|
|||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let todos = Todos::open(&dir.path().join("state.sqlite")).expect("open");
|
||||
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));
|
||||
assert_eq!(todos.list(Some(SUBSYSTEM)).expect("list").len(), 1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue