feat(cache): purge stale cargo cache entries in retention run

TODO 2.9 recorded a retention policy that warned about low disk while freeing
0.0 GB twice in a row, because it governed artifacts, logs and IP leases -- 52
KB in total on this host -- while 195 G of backups sat outside its reach.
Adding an unbounded directory of ~645 MB entries, one per Cargo.lock change,
without extending retention in the same change would repeat that at a larger
scale.

The window is the tighter of the global retention and the cache's own, so the
aggressive low-disk mode reaches the cache rather than stopping at the light
directories.

Sizes are read before deleting, not after. Summing st_size over files that have
already been unlinked yields 0 -- which is exactly how a policy ends up
reporting that it freed nothing while doing real work. A test asserts the
reported figure.
This commit is contained in:
2026-07-22 21:06:04 +02:00
parent 2678b76297
commit 15b1676375
2 changed files with 176 additions and 1 deletions
+44 -1
View File
@@ -14,7 +14,8 @@ from pathlib import Path
import click
from ci_orchestrator.config import load_config
from ci_orchestrator.cargo_cache import CargoCacheStore
from ci_orchestrator.config import Config, load_config
@click.group()
@@ -93,6 +94,7 @@ def retention_run(
_purge_old_dirs(art_path, "artifact", cutoff, what_if=what_if)
_purge_old_dirs(log_path, "log", cutoff, what_if=what_if)
_purge_cargo_cache(config, effective_days, what_if=what_if)
usage_after = shutil.disk_usage(str(check_path))
free_gb_after = usage_after.free / (1024**3)
@@ -132,3 +134,44 @@ def _purge_old_dirs(base: Path, label: str, cutoff: datetime, *, what_if: bool)
else:
shutil.rmtree(d, ignore_errors=True)
click.echo(f"[RetentionPolicy] Purged {label} ({age_days}d): {d.name}")
def _purge_cargo_cache(config: Config, effective_days: int, *, what_if: bool) -> None:
"""Purge stale cargo cache entries.
Entries are the heaviest thing this policy governs -- hundreds of megabytes
each, one per Cargo.lock change -- so leaving them out would repeat TODO
2.9, where retention warned about low disk while freeing 0.0 GB because
everything that actually weighed was outside its reach.
The window is the tighter of the global retention and the cache's own, so
the aggressive low-disk mode reaches the cache too.
"""
if not config.cargo_cache.enabled:
click.echo("[RetentionPolicy] cargo cache: disabled — skipping.")
return
days = min(effective_days, config.cargo_cache.retention_days)
cutoff = datetime.now(tz=UTC) - timedelta(days=days)
store = CargoCacheStore(config.cargo_cache.root)
# Size the victims first: after a real prune the files are gone and their
# bytes are unreadable, which is how a policy ends up reporting 0.0 GB
# freed while doing real work (TODO 2.9).
doomed = store.prune_older_than(cutoff, what_if=True)
if not doomed:
click.echo(
f"[RetentionPolicy] cargo cache: nothing to purge "
f"(cutoff: {cutoff.strftime('%Y-%m-%d')})."
)
return
freed_mb = sum(p.stat().st_size for p in doomed if p.exists()) / 1_048_576
if not what_if:
store.prune_older_than(cutoff)
verb = "WhatIf: would purge" if what_if else "Purged"
click.echo(
f"[RetentionPolicy] {verb} {len(doomed)} cargo cache "
f"{'entry' if len(doomed) == 1 else 'entries'} ({freed_mb:.0f} MB)"
)