You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(cleanup): give runner-downloads an ownership, age, and liveness predicate (#10584)
* fix(cleanup): give runner-downloads an ownership, age, and liveness predicate
`runs_service::cleanup_runner_downloads` was an unconditional
`fs::remove_dir_all` of `<artifact-root>/runner`. Its only checks were
path-containment ones — `--run-id` requires `--runner`, each filter must be
a single normal path component, the root must be a real directory — which
prove the deletion stays inside the cache and prove nothing about whether
the bytes are dead. `runner-downloads` was also swept by a bare `homeboy
cleanup --apply`, so artifacts an operator pulled seconds earlier were
deleted by an unrelated sweep.
That tree has exactly one writer: the default output path of
`download_remote_artifact`, `<artifact-root>/runner/<runner-id>/<run-id>/
<file>`. Every caller of it is a fetch someone asked for (`runs artifact
get`, `runs artifacts --pull`, `lab apply`, evidence mirroring, the HTTP
artifact endpoint), and `runs artifact get` hands that path back as the
location of the operator's file.
The predicate now requires, per cache directory:
- ownership by the canonical `<runner-id>/<run-id>` name shape, with no
symlink at either level. The database is deliberately not joined: bytes
here are written before, and usually without, any local artifacts row, so
row absence is the normal state of a download that is succeeding.
- a fixed 24h floor (`cleanup::RUNNER_MIN_AGE_HOURS`) over the *newest* byte
anywhere in the subtree, so one fresh pull re-arms the whole directory.
Not operator-overridable.
- a non-terminal-run veto read from the observation store in the retain
direction only. A missing row never authorizes removal.
- fail closed on every uncertainty: unreadable or future-dated mtime,
unwalkable subtree, out-of-root path, unopenable store, or a truncated
running-run scan all retain.
Removal is per cache directory rather than whole-root, so a stale cache and
a fresh one under the same runner are decided independently; the cache root
is never removed and an emptied `<runner-id>` directory is pruned only by a
non-recursive `remove_dir`. Sizes stay advisory and move the verdict in
neither direction. `--runner`/`--run-id` narrow which candidates are
considered and never waive a check.
The category is also withheld from the bare sweep. The predicate fixes the
acute data-loss case but cannot fix the remaining one: the writer emits the
same name shape for an operator's deliberate pull and for an internal
auto-fetch, so ownership of *intent* cannot be proven by name. Until the
writer tags its output, `--include runner-downloads` is the honest contract.
`cleanup retained-storage` still accounts for the bytes, now split into what
a sweep would reclaim and what it is holding on to, and still names the
reclaim command.
The specialist resolves its inspection budget through
`resolve_cleanup_policy` and echoes the policy, matching the other delete
paths unified in #10562. It carried no `default_value_t` retention literal,
so it was not drifting — it was simply not participating.
Fixes#10564
* fix(cleanup): derive Debug on CleanupFilters
Workspace Tests Compile failed with E0277: assert macros in the new
tests format CleanupFilters, which had no Debug impl. The CLI Reference
Docs gate failed for the same reason -- it runs cargo test -p homeboy-cli
to regenerate, so it could not compile either. The regenerated doc tree
is byte-identical to the checked-in one; the docs were never stale.
---------
Co-authored-by: chubes-bot <266378653+homeboy-ci[bot]@users.noreply.github.com>
"{} cached runner download(s) past the fixed {}s age floor with no non-terminal owning run ({} file(s), {} directory(ies))",
556
+
downloads.planned_count,
557
+
downloads.min_age_seconds,
558
+
downloads.file_count,
559
+
downloads.directory_count
549
560
),
550
561
owner:"homeboy".to_string(),
551
562
run_id:None,
552
563
liveness:LIVENESS_RECLAIMABLE.to_string(),
564
+
age:age_bucket(downloads.min_age_seconds),
565
+
age_seconds:Some(downloads.min_age_seconds),
566
+
size_bytes: downloads.planned_size_bytes,
567
+
reference: downloads_root.clone(),
568
+
});
569
+
}
570
+
if downloads.skipped_count > 0{
571
+
records.push(RetainedStorageRecord{
572
+
category:"runner_downloads".to_string(),
573
+
reason:format!(
574
+
"{} cached runner download(s) retained: younger than the age floor, claimed by a non-terminal run, or not the canonical <runner>/<run> shape; bytes not measured",
575
+
downloads.skipped_count
576
+
),
577
+
owner:"homeboy".to_string(),
578
+
run_id:None,
579
+
liveness:"lifecycle_pinned".to_string(),
553
580
age:"unknown".to_string(),
554
581
age_seconds:None,
555
-
size_bytes: downloads.size_bytes,
556
-
reference: downloads.root.display().to_string(),
582
+
// Advisory-signal rule: retained entries are deliberately not
583
+
// measured, so a zero here is "not measured", never "empty".
0 commit comments