fix(prune-cache): stop the aside sweeper depending on timing it cannot see
Review findings on #2. The first is the one that mattered: the sweeper this PR added had the shape the PR exists to remove. Pass A renames a candidate aside; pass B's sweep sees an aside with no readers and reclaims it; A then finds a reader and restores. `rm -rf` traverses fd-relative, so the rename does not stop it and A can republish a half-emptied tree under a live cache name. `capacity: 1` bounds it today, which is exactly the kind of reason this PR was written to stop relying on. The unlink itself was never the problem — the ordering proof covers it under any interleaving, since the aside name only exists after the evicting pass's rename. What was missing is that an aside with no readers is indistinguishable from one a pass has just created and not yet decided about. The sweeper now leaves an aside alone until it has settled (EVICTION_ASIDE_SETTLE_SECONDS, default 60), which separates the two without having to identify the pass that created it — a PID is meaningless across the job containers these passes run in, and recycles. Read from ctime, not mtime: rename(2) updates the first and leaves the second at whenever the cache was last written, which is the signal list_by_lru wants and the wrong one here. That is a bound, not a construction, and both the code comment and the README now say which of the two properties is which instead of asserting the broader one. Also from the review: a pass that declined every dead cache it found no longer signs off with "no dead-branch caches found", and evict_dir no longer promises a later reclamation of an aside that is already gone. Scenario 14 covers the settle window against the script's own default, with nothing faked — the directory really was set aside a moment ago. Scenario 4 gains the summary assertion. 31 -> 37 assertions; each new gate verified red by defeating it alone in a scratch copy.
This commit is contained in:
@@ -182,9 +182,14 @@ not collapsed into one:
|
||||
rename cannot resolve the path at all and starts cold instead. A cache
|
||||
claimed inside that window is put back under its own name, and one whose
|
||||
name a concurrent seed has taken in the meantime is left aside and reclaimed
|
||||
by a later pass once its readers drain. Until this was structural it was
|
||||
merely policy — snapshots belong to protected refs, protected refs are never
|
||||
eviction candidates — which is a property held by vigilance rather than by
|
||||
by a later pass once its readers drain. That later pass leaves an aside
|
||||
directory alone until it has been set aside for a minute — not for the
|
||||
unlink's sake, which the ordering proof above already covers, but so that a
|
||||
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
|
||||
construction.
|
||||
- **Every other way the source can change mid-clone is detected, not
|
||||
prevented.** A `seed-fallback-dir` pointing at a directory something else
|
||||
@@ -199,9 +204,9 @@ not collapsed into one:
|
||||
residual is capped at one deferred generation per publisher ref, and its
|
||||
real cost is close to inode count rather than byte count, since the
|
||||
artifacts are hardlinked to whatever cloned them. A declined eviction is
|
||||
bounded the same way: the cache stays, either under its own name or aside
|
||||
awaiting the next pass, and is never unlinked under the job that claimed
|
||||
it.
|
||||
never unlinked under the job that claimed it; what it costs meanwhile is
|
||||
disk, normally as the cache restored under its own name and otherwise as one
|
||||
set aside for a later pass to reclaim.
|
||||
|
||||
**Eviction** runs three passes: caches for branches that no longer exist on
|
||||
origin are removed unconditionally; then, only if free space is under the
|
||||
|
||||
Reference in New Issue
Block a user