docs(cache): state the .publish-* gap as a ticket, not a present state

The contract block asserted that neither side reclaims .publish-new- 'today'.
True when written and about to stop being true: gitdan#30 tracks adding both
.publish-* prefixes to the arbiter's enumeration, and a sibling track is
landing it this round. A comment that dates itself against a merge in flight
is worse than no comment.

Rewords all four sites (cache-lib.sh, publish-snapshot.sh, and README's table
row and prose) to reference gitdan#30 and keep the mechanism that made the
shape worth catching — .publish-new- is tagged per job per run exactly as
.stage- is — rather than the arbiter's momentary contents. The rule itself is
unchanged; it is the durable part, and it is what found this.

Adds the counting check while there: the five names here and the prefixes
ci-cache-reclaim.sh enumerates are meant to be the same length, so a mismatch
is the cheapest signal that one side gained a shape without telling the other.
This commit is contained in:
2026-08-24 12:56:18 -05:00
parent 0cf6cc5973
commit 0118c28f01
3 changed files with 28 additions and 15 deletions
+10 -3
View File
@@ -245,7 +245,7 @@ block is the canonical description; this side owns the names.
| name | produced by | if the job dies holding it | | name | produced by | if the job dies holding it |
|---|---|---| |---|---|---|
| `.stage-<tag>` | `cache-lib.sh`, `hardlink_clone_into()` | stranded; only the arbiter reclaims it | | `.stage-<tag>` | `cache-lib.sh`, `hardlink_clone_into()` | stranded; only the arbiter reclaims it |
| `.publish-new-<tag>` | `publish-snapshot.sh` | stranded, and not in the arbiter's list | | `.publish-new-<tag>` | `publish-snapshot.sh` | stranded; see `daniel/gitdan#30` |
| `.publish-old-<key>-<tag>` | `publish-snapshot.sh` | swept by the next publish of that key | | `.publish-old-<key>-<tag>` | `publish-snapshot.sh` | swept by the next publish of that key |
| `.evicting-<name>-<pid>` | `prune-cache.sh`, `evict_dir()` | swept at the start of the next prune pass | | `.evicting-<name>-<pid>` | `prune-cache.sh`, `evict_dir()` | swept at the start of the next prune pass |
| `.reading-<source>-<tag>` | `cache-lib.sh`, `reader_lock_acquire()` | not garbage — see below | | `.reading-<source>-<tag>` | `cache-lib.sh`, `reader_lock_acquire()` | not garbage — see below |
@@ -263,8 +263,15 @@ renaming one. That script enumerates by explicit prefix rather than by dotglob
stream alongside the trees they protect — so a name it has not been told about stream alongside the trees they protect — so a name it has not been told about
is not handled conservatively, it is invisible, and an unreclaimed staging tree is not handled conservatively, it is invisible, and an unreclaimed staging tree
is a full clone of a multi-GB target dir on the one volume whose entire problem is a full clone of a multi-GB target dir on the one volume whose entire problem
is disk. `.publish-new-` is the standing example: it strands exactly as is disk. `.publish-new-` is the worked example: the two `.publish-*` names were
`.stage-` does and is in neither side's sweep. outside the contract when it was written, and `.publish-new-` strands exactly
as `.stage-` does, so it is the one that needed catching. Bringing both under
the arbiter's enumeration is tracked as `daniel/gitdan#30`.
The two lists are meant to be the same length — the five names above, and the
prefixes `ci-cache-reclaim.sh` enumerates. A mismatch means one side gained a
shape without telling the other, which is the drift the rule exists to catch
and the cheapest thing to check.
**The staleness constants are part of the same contract, and that half has a **The staleness constants are part of the same contract, and that half has a
direction.** `CACHE_READ_STALE_SECONDS` (`cache-lib.sh`) and direction.** `CACHE_READ_STALE_SECONDS` (`cache-lib.sh`) and
+11 -6
View File
@@ -55,12 +55,17 @@
# is invisible: an unreclaimed staging tree is a full clone of a multi-GB # is invisible: an unreclaimed staging tree is a full clone of a multi-GB
# target dir on the one volume whose entire problem is disk. # target dir on the one volume whose entire problem is disk.
# #
# Of the five above, the arbiter enumerates `.stage-`, `.evicting-` and # The two `.publish-*` names predate the contract and were outside it when this
# `.reading-`. The two `.publish-*` names predate the contract and are not in # block was written — the rule catching an uncovered shape on its first
# it, which is why `.publish-new-` — tagged per job per run exactly as # application. `.publish-new-` is the one that mattered: it is tagged per job
# `.stage-` is — is a shape neither side reclaims today. Closing that is a # per run exactly as `.stage-` is, so a publisher killed before the swap
# change over there, not here; the rule above is what stops the list growing # strands a tree under a name no later run of that script matches, which is
# another one. # precisely the shape only the arbiter can reach. Bringing both in is tracked
# as daniel/gitdan#30.
#
# The two lists are meant to be the same length. A shape here without a prefix
# there is one side having changed without telling the other, and it is
# cheapest to notice by counting.
# #
# The two staleness constants the arbiter mirrors are part of the same # The two staleness constants the arbiter mirrors are part of the same
# contract, and that half has a direction to it — see CACHE_READ_STALE_SECONDS # contract, and that half has a direction to it — see CACHE_READ_STALE_SECONDS
+7 -6
View File
@@ -55,12 +55,13 @@ DST=$(snapshot_dir_for "$ROOT" "$OWN_KEY")
# `.publish-old-` and `.publish-new-` are dot-prefixed names under a cache # `.publish-old-` and `.publish-new-` are dot-prefixed names under a cache
# root, so the cross-repo contract at the top of cache-lib.sh covers them: # root, so the cross-repo contract at the top of cache-lib.sh covers them:
# renaming either, or adding a third, requires a matching prefix in # renaming either, or adding a third, requires a matching prefix in
# daniel/gitdan's ci-cache-reclaim.sh. Neither is in its list today, and # daniel/gitdan's ci-cache-reclaim.sh. Both were outside its list when that
# `.publish-new-` is the shape that most needs to be — it is tagged per job per # contract was written, and `.publish-new-` is why it mattered: it is tagged
# run, so a publisher killed between staging its snapshot and the swap below # per job per run, so a publisher killed between staging its snapshot and the
# strands a full hardlink clone that no later run of this script will ever # swap below strands a full hardlink clone that no later run of this script
# match. `.publish-old-` is milder: the sweep further down reclaims it on the # will ever match. `.publish-old-` is milder the sweep further down reclaims
# next publish of the same key. # it on the next publish of the same key. Bringing both under the arbiter's
# enumeration is tracked as daniel/gitdan#30.
OLD="${ROOT}/.publish-old-${OWN_KEY}-${TAG}" OLD="${ROOT}/.publish-old-${OWN_KEY}-${TAG}"
SNAP_NAME=$(basename "$DST") SNAP_NAME=$(basename "$DST")
GRACE="${CACHE_READ_GRACE_SECONDS}" GRACE="${CACHE_READ_GRACE_SECONDS}"