fix(prune): stop protecting publisher target dirs under disk pressure
CI / shellcheck + selftests (pull_request) Successful in 1m48s
CI / shellcheck + selftests (pull_request) Successful in 1m48s
A publisher branch's target-<ref> was protected identically to its snapshot-<ref>, so it was never a pressure-pass candidate however old and however tight the disk. With one branch's target dir permanently resident alongside its snapshot, a second branch had no room to seed, and every PR that night needed a hand eviction between runs (daniel/gitdan-actions#24). A publisher's target dir is a convenience cache its own next run reseeds from the snapshot, so losing it under pressure is cheap; nothing downstream depends on it surviving. Only the snapshot stays protected in the pressure and self-clear passes. Unprotecting the target dir outright surfaced a second bug the fix would otherwise have shipped: a branch's tip is trivially an ancestor of itself, so once a protected ref's target dir was no longer skipped before reaching the merged-branch check, pass 1 read it as "merged into itself" and deleted it unconditionally on every run, independent of disk pressure. is_protected_from_liveness keeps a protected ref's target dir out of pass 1 alone, so it stays an ordinary pressure-pass candidate without ever reaching that check. Scenario 3b in the selftest red-proves this against the unprotect-only version of the fix. Also corrects the header's inode-sharing claim, measured false on the live volume by daniel/zemyna#1073: publish-snapshot.sh unshares every executable after its cp -al, and executables are most of the tree by bytes, so a snapshot eviction is a real, large disk cost rather than the near-free one the old text described — the target-before-snapshot ordering still holds, now for the warm-start reason alone plus that cost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkSjXXtU6JYcN2vPntWhfb
This commit is contained in:
@@ -247,8 +247,8 @@ not collapsed into one:
|
||||
pass still deciding about one is never mistaken for a pass that died holding
|
||||
it. That settle window is a bound rather than a construction, and it is the
|
||||
only part of this that is. Until the rest of it was structural it was merely
|
||||
policy — snapshots belong to protected refs, protected refs are never
|
||||
eviction candidates — a property held by vigilance rather than by
|
||||
policy — snapshots belong to protected refs, and a protected ref's snapshot
|
||||
is never an eviction candidate — a property held by vigilance rather than by
|
||||
construction.
|
||||
- **Every other way the source can change mid-clone is detected, not
|
||||
prevented.** A `seed-fallback-dir` pointing at a directory something else
|
||||
@@ -271,12 +271,21 @@ not collapsed into one:
|
||||
available to the clone that follows. Caches for branches that are DEAD are
|
||||
removed unconditionally; then, only if free space is under the requirement,
|
||||
live caches are evicted oldest-first; then, as a last resort, this run's own
|
||||
cache. Protected refs, the source this run is about to clone, and any cache
|
||||
held open by a running job are never candidates. Within the pressure pass,
|
||||
`target-*` directories are evicted before `snapshot-*` ones — the reverse of
|
||||
the obvious order, because a snapshot is hardlinked to everything cloned from
|
||||
it, so removing one frees almost no real bytes while costing every future PR
|
||||
its warm start.
|
||||
cache. A protected ref's *snapshot*, the source this run is about to clone,
|
||||
and any cache held open by a running job are never candidates in the pressure
|
||||
or self-clear passes. A protected ref's own *target* dir is an ordinary
|
||||
pressure-pass candidate, since it is a convenience cache the publisher's next
|
||||
run reseeds from the snapshot — but it is excluded from the liveness pass
|
||||
alone, because a branch's tip is trivially an ancestor of itself, and without
|
||||
that exclusion the merged-branch signal would read a publisher's own target
|
||||
dir as merged into itself and delete it every run, unconditionally. Within the
|
||||
pressure pass, `target-*` directories are evicted before `snapshot-*` ones:
|
||||
losing a target dir is cheap for exactly that reseeding reason, while
|
||||
evicting a snapshot forces every subsequent PR to start cold and, measured on
|
||||
the live volume (daniel/zemyna#1073), frees real disk rather than the
|
||||
near-nothing a shared-inode hardlink clone would suggest — `publish-snapshot.sh`
|
||||
unshares every executable after its `cp -al`, and executables are most of the
|
||||
tree by bytes.
|
||||
|
||||
**A branch is dead in two ways, and neither signal makes the other
|
||||
redundant.** The first is that the branch is gone from origin. The second is
|
||||
@@ -474,7 +483,7 @@ directory; the line just doesn't say which.
|
||||
|---|---|---|
|
||||
| `cache-root` | `/cache` | mount point of the persistent volume inside the job container |
|
||||
| `cache-lineage` | *(empty)* | one directory level under `cache-root`, for a second job building the same ref for a different target or profile — see [Multiple jobs in one workflow](#multiple-jobs-in-one-workflow) |
|
||||
| `protected-branches` | `dev main` | refs that publish snapshots and are never evicted |
|
||||
| `protected-branches` | `dev main` | refs that publish snapshots, whose snapshots are never evicted (their target dirs are ordinary pressure-pass candidates) |
|
||||
| `min-free-percent` | `0` | an ADDITIONAL free-space floor, as a percentage of the volume. The gate is derived per run from what the seed is about to clone; this only ever raises it |
|
||||
| `restore-mtimes` | `true` | restore tracked-file mtimes from git history |
|
||||
| `prune` | `true` | run the eviction pass — before the seed, so what it frees is available to the clone |
|
||||
|
||||
Reference in New Issue
Block a user