test(galaxy): shutdown stream-cancel signal from the iterator's exit, not a token registration (#856)

Found while load-verifying #856: ShutdownAsync_cancels_the_event_stream_and_releases_the_pump
timed out 7/700 under parallel load. The fake's StreamCancelled was set by a
CancellationToken registration scoped by `using` inside the async iterator.
On Cancel() the newer ReadAllAsync registration runs first and resumes the
iterator on a pool thread, whose `using` can dispose our registration while
Cancel() is still walking the callback list - so it never runs. Signal from
the iterator's finally (when the token is cancelled) instead, which also
proves the stream actually ended. 0/700 after.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
Joseph Doherty
2026-11-11 10:00:44 -05:00
parent 988046cbe4
commit 90de38910d
@@ -68,10 +68,22 @@ public sealed class GalaxyDriverShutdownTests
public async IAsyncEnumerable<MxEvent> StreamEventsAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
Interlocked.Increment(ref StreamsOpened);
using var _ = cancellationToken.Register(() => StreamCancelled.TrySetResult());
await foreach (var ev in _events.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
// Signal from the iterator's own exit, not from a token registration: a registration made
// here is disposed by the `using` when the cancelled ReadAllAsync resumes the body on a
// pool thread — which can happen while Cancel() on the pump's thread is still walking its
// callback list, unregistering ours before it runs (CTS runs callbacks newest-first, and
// ReadAllAsync's is newer). Observed ~1% under parallel load as a 10 s timeout. A finally
// also proves the stronger fact: the stream actually ended because it was cancelled.
try
{
yield return ev;
await foreach (var ev in _events.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
yield return ev;
}
}
finally
{
if (cancellationToken.IsCancellationRequested) StreamCancelled.TrySetResult();
}
}