zed/crates/eval_cli/zed_eval/cleanup.py
Anant Goel 10f501d700
eval_cli: Add remote benchmark orchestration (#59802)
Summary:

- Add the `zed-eval` Python CLI for Modal/Harbor/Pier benchmark
orchestration, including content-addressed remote builds, run/suite
management, reporting, rejudge, baseline, and cleanup workflows.
- Extend `eval-cli` for remote evals with provider/model overrides and
step/tool-call metrics in `result.json`.
- Add install/source-run helper scripts so `zed-eval` can be installed
or run from the checkout without manually setting `PYTHONPATH`.
- Harden the remote harness wrappers around exit-code preservation,
archive extraction, custom secret wiring, and Harbor/Pier option parity,
with regression coverage.

Testing:

- Using the CLI for two weeks
- `PYTHONPATH=crates/eval_cli python3 -m compileall -q
crates/eval_cli/zed_eval`
- `uv run --project crates/eval_cli/zed_eval python -m unittest discover
-s crates/eval_cli/zed_eval/tests`
- `bash -n crates/eval_cli/script/install-zed-eval
crates/eval_cli/script/zed-eval`
- `cargo check -p eval_cli`
- `cargo fmt --package eval_cli -- --check`
- `cargo test -p eval_cli --no-run`
- `./script/clippy -p eval_cli`

Release Notes:

- N/A
2026-06-24 15:32:41 +00:00

93 lines
3.2 KiB
Python

"""Prune stale build artifacts and build cache from the shared volume.
Eval results are sacrosanct: this only ever touches the build-related top-level
directories listed in `PRUNABLE_DIRS`. `runs/` (the eval outputs) is never in
that set and is never walked, so a bug here cannot delete results.
The logic is pure-filesystem (no Modal dependency) so it can be unit-tested
against a temporary directory; `modal_app` wraps it with volume reload/commit and
a daily schedule.
"""
from __future__ import annotations
import datetime
import shutil
import time
from pathlib import Path
from typing import Any
# The ONLY top-level volume directories cleanup may modify. Eval results live in
# runs/, which is deliberately absent here.
PRUNABLE_DIRS = frozenset({"builds", "build-locks", "tmp"})
DEFAULT_BUILD_RETENTION_DAYS = 14.0
DEFAULT_LOCK_TTL_HOURS = 6.0
DEFAULT_TMP_MAX_AGE_HOURS = 24.0
def _age_days(path: Path) -> float:
try:
return (time.time() - path.stat().st_mtime) / 86400.0
except OSError:
return 0.0
def prune_artifacts(
data_root: Path | str,
*,
dry_run: bool = False,
build_retention_days: float = DEFAULT_BUILD_RETENTION_DAYS,
lock_ttl_hours: float = DEFAULT_LOCK_TTL_HOURS,
tmp_max_age_hours: float = DEFAULT_TMP_MAX_AGE_HOURS,
) -> dict[str, Any]:
data_root = Path(data_root)
removed: dict[str, list[Any]] = {
"builds": [],
"locks": [],
"tmp": [],
}
# 1. Stale content-addressed builds, aged by their READY marker (or the dir
# itself for never-finished builds). Cheap to rebuild on demand.
builds_dir = data_root / "builds"
if builds_dir.is_dir():
for build_dir in sorted(builds_dir.iterdir()):
if not build_dir.is_dir():
continue
ready = build_dir / "READY"
age = _age_days(ready if ready.exists() else build_dir)
if age > build_retention_days:
removed["builds"].append(
{"build_id": build_dir.name, "age_days": round(age, 1)}
)
if not dry_run:
shutil.rmtree(build_dir, ignore_errors=True)
# 2. Orphaned single-flight build leases left by crashed builds.
locks_dir = data_root / "build-locks"
if locks_dir.is_dir():
for lock in sorted(locks_dir.glob("*.json")):
if _age_days(lock) * 24.0 > lock_ttl_hours:
removed["locks"].append(lock.name)
if not dry_run:
lock.unlink(missing_ok=True)
# 3. Orphaned temp build dirs from failed/raced builds.
tmp_builds = data_root / "tmp" / "builds"
if tmp_builds.is_dir():
for entry in sorted(tmp_builds.iterdir()):
if _age_days(entry) * 24.0 > tmp_max_age_hours:
removed["tmp"].append(entry.name)
if not dry_run:
if entry.is_dir():
shutil.rmtree(entry, ignore_errors=True)
else:
entry.unlink(missing_ok=True)
return {
"dry_run": dry_run,
"at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"removed": removed,
"counts": {key: len(value) for key, value in removed.items()},
}