Files
at-container-registry/pkg
Evan JarrettandClaude Opus 5 3589473feb appview: run the hold purge on a worker pool, not the request context
Deleting a tag while over quota could leave the user in the worst available
state. DeleteTagHandler deleted the tag and manifest rows first, then called
PurgeOnHold, which bounded itself at 10s against the *request* context. The
UpCloud load balancer in front of the appview cuts at its default backend
timeout at about the same moment, wins the race, hands the client a 504 and
cancels that context, killing the purge partway. The appview logged a warning
and returned 200.

So: gateway error, nothing freed, still locked out, the image gone from the UI
so the purge cannot be retried through it, blobs orphaned until the hold's GC,
and the appview considering it a success. Measured on production at
10.002367218s.

Purges now go to a fixed pool of 4 workers rooted at context.Background(), so
they survive the request ending. Following the shape of the hold's startJob
helper, minus the progress fragment, since nobody is watching a purge.

The buffer is bounded at 256 and sheds with an ERROR rather than growing: an
unbounded queue turns a slow hold into an appview memory leak. Submissions are
deduplicated on holdDID|manifestURI so a double-clicked delete does one purge
and one service-token fetch. The channel send happens under the mutex that
guards close, so a concurrent drain cannot send on a closed channel, and the
drain is wired into both exit paths before logging shuts down.

Failures are now classified and surfaced instead of swallowed: transient ones
retry three times under a 90s budget (the hold's purge is idempotent), an
unauthorized third-party hold logs at DEBUG since it is expected, and anything
else that exhausts its retries logs at ERROR naming the manifest and hold, which
is enough to re-drive by hand.

Deliberately not reordered. Purge-first-then-delete requires waiting for the
purge to know whether to delete, which puts the 10s call straight back on the
request. So the orphaned-blob window remains, materially narrower but real: a
purge that exhausts its retries still leaves blobs referenced by nothing until
the hold's GC, and there is no row left to say so. Closing that needs a durable
pending-purge record, which was judged out of proportion here.

server.go in this commit also carries one line belonging to the next one, the
token handler's display-name wiring, since the two changes landed in the same
file concurrently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9
2026-09-02 22:31:27 -05:00
..
2026-05-11 19:53:13 -05:00