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:
@@ -11,8 +11,13 @@
|
||||
# Waiting for pressure to notice means paying for dead caches until then.
|
||||
# 2. LIVE BRANCH SURVIVES despite being OLDER than the dead one — liveness,
|
||||
# not age, is what decides pass 1.
|
||||
# 3. PROTECTED REFS NEVER EVICTED under forced disk pressure, even when
|
||||
# their caches are the oldest on disk and would rank first for LRU.
|
||||
# 3. A PROTECTED REF'S SNAPSHOT NEVER EVICTED under forced disk pressure,
|
||||
# even when it is the oldest on disk and would rank first for LRU — its
|
||||
# own TARGET dir is an ordinary candidate and goes (gitdan-actions#24).
|
||||
# 3b. AND A PROTECTED REF'S TARGET DIR SURVIVES PASS 1 ANYWAY: its tip is
|
||||
# trivially an ancestor of itself, so the merged-branch signal must never
|
||||
# be allowed to evaluate it, or pass 1 — unconditional, not gated on
|
||||
# pressure — would delete it every run.
|
||||
# 4. LOCKED CACHE PROTECTED even when dead, old, and under pressure — and
|
||||
# the pass's closing summary agrees with the decline it just logged,
|
||||
# rather than reporting that it found nothing.
|
||||
@@ -139,13 +144,34 @@ assert_kept "$root/target-$LIVE" "live branch survives despite an older marker t
|
||||
assert_log "no matching branch on origin" "eviction reason reported"
|
||||
|
||||
echo
|
||||
echo "=== 3: protected refs never evicted under forced pressure ==="
|
||||
echo "=== 3: protected refs' SNAPSHOTS never evicted under forced pressure ==="
|
||||
reset_cache
|
||||
run_prune "1000000 1000" # 0.1% free
|
||||
assert_kept "$root/target-$DEV" "dev's target dir survives disk pressure"
|
||||
assert_kept "$root/snapshot-$DEV" "dev's snapshot survives disk pressure"
|
||||
assert_kept "$root/target-$MAIN" "main's target dir survives disk pressure"
|
||||
assert_kept "$root/snapshot-$MAIN" "main's snapshot survives disk pressure"
|
||||
# Their TARGET dirs are ordinary candidates and go under the same pressure —
|
||||
# gitdan-actions#24: protecting them starved every second branch of room to
|
||||
# seed. Asserted here (gone, not kept) so this scenario still red-proves the
|
||||
# snapshot half if a future change reintroduces target protection.
|
||||
assert_gone "$root/target-$DEV" "dev's target dir is an ordinary pressure-pass candidate"
|
||||
assert_gone "$root/target-$MAIN" "main's target dir is an ordinary pressure-pass candidate"
|
||||
|
||||
echo
|
||||
echo "=== 3b: a protected ref's target dir is NOT pruned by pass 1's liveness ==="
|
||||
# The regression this fix could introduce and the selftest above cannot see:
|
||||
# a protected branch's tip is trivially an ancestor of itself, so once its
|
||||
# target dir stopped being excluded from pass 1 altogether, is_merged_dead
|
||||
# read it as "merged into itself" and pass 1 — unconditional, not gated on
|
||||
# pressure — deleted it on every single run. Plenty of free space, so only
|
||||
# pass 1 can be responsible for anything gone here.
|
||||
reset_cache
|
||||
run_prune "1000000 900000" # 90% free: no pressure at all
|
||||
assert_kept "$root/target-$DEV" "dev's target dir survives pass 1 despite being its own ancestor"
|
||||
assert_kept "$root/target-$MAIN" "and so does main's"
|
||||
if grep -q "merged into" "$scratch/log"; then
|
||||
fail "a protected ref's own target dir was evaluated by the merged-branch signal at all"
|
||||
fi
|
||||
ok "no protected ref's own target dir reaches the merged-branch check"
|
||||
|
||||
echo
|
||||
echo "=== 9: own cache never evicted by a sibling pass ==="
|
||||
|
||||
Reference in New Issue
Block a user