🎣 Unify close/cancel/value precedence across all packages#10
Merged
Conversation
Every package now resolves RecvContext as closed > cancelled > value and SendContext as closed > cancelled, so an entry-time termination reports ErrClosed even for an already-cancelled ctx and a cancelled ctx is observable even when a value is ready. No path consumes-and-discards a value: it is delivered or left in the queue/ring for another receiver. Adds a root-level conformance table exercising the ordering across all seven architectures, plus internal/parked, a sleep-free helper that waits on the runtime's own view of a goroutine so tests provably reach the parked arm they claim to cover. chancore drops BufferedRecv (mpmc's recv loops need a per-handle close arm) and makes Dead/Ready required. Signed-off-by: Andres Morey <root@andresmorey.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The context-aware methods disagreed with each other about what wins when a
close, a cancellation, and a ready value are all in play.
SendContextinchancoretestedChClosedabove thectxcheck butDeadbelow it, so thesame logical condition — this sender is finished — reported
ErrClosedorctx.Err()depending on which arm happened to observe it.RecvContextpreferred a ready value over a cancelled context in
broadcastandwatch,which meant a receiver looping against a fast enough sender took the value every
iteration and never noticed its own shutdown signal. Nothing pinned any of this,
so each package had drifted on its own.
This settles one ordering — closed > cancelled > value — and applies it
everywhere. An entry-time termination reports
ErrClosedeven for analready-cancelled
ctx, because that is the durable answer and a retry with afresh context would only return it again. A cancelled
ctxon a live handlereports
ctx.Err()even when a value is ready, which is what keeps cancellationobservable under load. No path consumes-and-discards: a value is either
delivered to the caller or left in the queue/ring for another receiver.
Fixing the ordering surfaced two things worth their own attention.
broadcast'scancelled path had grown its own copy of the terminal decision and, with it, a
bare return that skipped
unregisterLocked— a receiver abandoned onctx.Err()held its ring slot for the hub's lifetime. And the existing testscould not actually cover the parked arms: a
startedchannel signalled justbefore a blocking call proves the goroutine was scheduled, not that it reached
the select, so the trigger raced the call's own fast path and the assertions
passed while covering the opposite of what they claimed.
Key Changes
SendContext(closed > cancelled) andRecvContext(closed > cancelled > value) across all seven packages.chancore.BufferedSendgains aclosed()helper soChClosedandDeadarealways tested together and in one order, and rechecks both plus
ctxaftertaking
SendLock, since the readiness gate and the lock can each block for anunbounded time.
broadcast.RecvContextunregisters on the cancelled path. The wholeprecedence now runs as one ordered pass under
mu, so the terminal exit andits tear-down obligation cannot drift apart again.
mpmc.Recvdelegates toRecvContextwith a never-cancelled contextrather than duplicating the body — one place for precedence to change.
Measured at well under 1ns against the hand-inlined version.
conformance_test.go(new, root): a table driving all sevenarchitectures through the same ordering assertions via the module-wide
Sender/Receiverinterfaces, so a future package cannot quietly disagree.internal/parked(new): sleep-free waiting on the runtime's own view of agoroutine, so a test can prove the goroutine under test actually parked in the
arm it claims to exercise. Identity-based rather than count-based — a
goroutine left parked in the same frame by an earlier test would otherwise
satisfy the wait on its own.
chancore.BufferedRecvremoved. The receiver needs a per-handle closearm, so
mpmc's receive loops live inmpmc.DeadandReadyare nowrequired and typed
*CloseOnce; a send-side with no readiness conditionpasses an already-latched one rather than nil.
README.mdgains the precedence rules and both close-raceboundaries (a send racing the last receiver's close may deposit a value
nothing drains — the same guarantee a plain buffered channel gives);
CLAUDE.mdupdated for thechancorereshape.Checklist