mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 12:17:00 +00:00
Same class of bug as the firehose backfill, plus a second one found alongside it. Both take down the whole hold process. 1. send on closed channel. Subscribe spawns drainPendingJobs in its own goroutine, and it sends to sub.send without holding sb.mu. Unsubscribe closed sub.send under the lock, so a scanner disconnecting during the drain closed the channel out from under an in-flight send. The existing `case <-sub.done` guard did not help: done meant "writer goroutine exited" and was closed by handleWriter, which is a different event from unsubscribing. 2. close of closed channel. Unsubscribe closed sub.send unconditionally, but it is called from two places — handleWriter on write error, and handleReader in its defer. A scanner dropping mid-write hits both, and the slice-removal loop had no guard, so the second call fell straight through to the close. The unassign UPDATE ran twice for the same reason, which could return jobs a replacement scanner had already been handed. sub.send is now never closed. done is repurposed to mean "this subscriber is gone", closed only by Unsubscribe and guarded on whether the subscriber was actually still registered. That makes drainPendingJobs' existing done case correct, and handleWriter selects on done rather than ranging over send. dispatchJob was already safe — it sends under sb.mu, which excludes Unsubscribe. hold01 is unaffected in practice (scanner disabled, no shared secret), but seamark-hold runs the scanner continuously and is exposed on any scanner restart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>