Fix async enumerable source disposal ordering - #8265
Conversation
The GC/leak test (AsyncEnumerableSource_Releases_References_After_Cleanup) and the soak/churn test (AsyncEnumerableSource_Disposes_Every_Enumerator_Under_Churn) pass on BOTH the buggy and the fixed Ops.cs, so they provide no regression protection for akkadotnet#7381. Verified by reverting only the Ops.cs fix: both still pass (4/4 runs), while the two deterministic guards (Does_Not_Dispose_Enumerator_While_MoveNextAsync_Is_In_Flight and Does_Not_Dispose_CancellationTokenSource_While_MoveNextAsync_Is_In_Flight) fail as expected. The churn test's CountingAsyncEnumerator never enforces the no-overlap contract (DisposeAsync was always called in the old code too), and the GC test only exercised general teardown, not the disposal-ordering fix. Keep the two deterministic guards as the actual regression coverage; drop the two non-differentiating tests and their dedicated helpers (ResourceCounter, CountingAsyncEnumerable, CancelMidMoveThenReleaseAsync) to save CI time and remove GC-test flake risk.
|
Pushed a commit (84e3553) that drops two of the four new tests - Both pass whether or not the fix is in place, so neither actually guards the regression. I checked it directly - reverted just the The churn test can't catch it by construction - The two deterministic guards are the real coverage and they stay - they park a The |
|
Thanks, makes sense. |
| private readonly CancellationTokenSource _completionCts; | ||
|
|
||
| private IAsyncEnumerator<T> _enumerator; | ||
| private Task _inFlightMove; |
| { | ||
| await _enumerator.DisposeAsync(); | ||
| if (inFlightMove != null) | ||
| await inFlightMove.ConfigureAwait(false); |
There was a problem hiding this comment.
The ConfigureAwait(false) stuff is fine here because this is all running inside a detached task anyway
| } | ||
|
|
||
| _ = ProcessTask(); | ||
| _inFlightMove = ProcessTask(); |
- akkadotnet#8265: Fix async enumerable source disposal ordering - akkadotnet#8264: Improve BroadcastHub high-consumer wheel performance Cherry-picked from upstream/dev (c619025, 10b9831)
Summary
Fixes #7381.
Related: #6280, #6903.
Historical context: #6290, #6935.
Source.From(IAsyncEnumerable<T>)cleanup until any pendingMoveNextAsync()task has completed.MoveNextAsync()/DisposeAsync(), cancellation token source lifetime, deferred-cleanup reference release, and bounded cancel churn.Risk / tradeoff
Cleanup intentionally waits for the in-flight
MoveNextAsync()without a timeout. A non-cooperative async enumerator that ignores cancellation and never returns can therefore keep cleanup waiting and retain that materialization.This is deliberate: applying a bounded wait and then calling
DisposeAsync()would reintroduce the same overlappingMoveNextAsync()/DisposeAsync()race this PR fixes.Validation
git diff --check origin/dev..HEAD- cleandotnet test src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~AsyncEnumerableSource_Does_Not_Dispose_Enumerator_While_MoveNextAsync_Is_In_Flight|FullyQualifiedName~AsyncEnumerableSource_Does_Not_Dispose_CancellationTokenSource_While_MoveNextAsync_Is_In_Flight"- 2 passeddotnet test src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~AsyncEnumerableSource_Releases_References_After_Cleanup|FullyQualifiedName~AsyncEnumerableSource_Disposes_Every_Enumerator_Under_Churn"- 2 passeddotnet test src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~Akka.Streams.Tests.Dsl.AsyncEnumerableSpec"- 17 passeddotnet test src/core/Akka.Streams.Tests/Akka.Streams.Tests.csproj -c Release --no-restore- 1849 passed, 34 skipped, 0 failed