diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GalaxyDriverShutdownTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GalaxyDriverShutdownTests.cs index 5c2b9b94f..6c2b45481 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GalaxyDriverShutdownTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GalaxyDriverShutdownTests.cs @@ -68,10 +68,22 @@ public sealed class GalaxyDriverShutdownTests public async IAsyncEnumerable 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(); } }