fix: resolve pre-4.0 audit findings across streaming and cancellation paths#370
Merged
Conversation
… paths (#362-#369) Exception fidelity (#362): the Task returned by the IAsyncEnumerable processors' ExecuteAsync is now built from a TaskCompletionSource, so Task.Exception.InnerExceptions carries every failure (Task.WhenAll fidelity) instead of discarding all but the first. Applied to the worker pool, parallel, batch (whole failing batch preserved), and one-at-a-time (multi-fault item task preserved) processors via a new internal StreamingExecution helper. Abandonment semantics (#363, #364): breaking out of a result stream now cancels the processor's in-flight work, drains it within the 30s disposal window, and observes failures so they neither mask the propagating exception nor surface as UnobservedTaskException. The void twin no longer masks a mid-enumeration source failure with an in-flight task failure. The unbounded ProcessInParallel extension drains started tasks on mid-enumeration failure instead of abandoning them fire-and-forget. Ordering contract (#365): documented per method - bounded streaming and the awaitable extension yield source order, unbounded streaming and GetResultsAsyncEnumerable yield completion order - in XML docs and a new README section. ConfigureAwait gaps (#366): added ConfigureAwait(false) to every WithCancellation enumeration in the SelectMany/SelectManyAsync iterator families. Pre-cancelled token (#367): building a processor with an already-cancelled token now yields cancelled per-item tasks (TPL convention) instead of ArgumentException naming an internal constructor parameter. The registration in ProcessorLifecycle.Start fires synchronously on the fully built instance. CancelAll/Dispose race (#368): CancelAllCore tolerates a concurrent Dispose winning the race to the CancellationTokenSource, matching the streaming processors' existing handling. Single-use enforcement (#369): a second ExecuteAsync call throws InvalidOperationException eagerly at the call site; a call after disposal throws ObjectDisposedException naming the processor type. Fixes #362, #363, #364, #365, #366, #367, #368, #369.
Greptile SummaryThis PR updates streaming execution and cancellation behavior before the v4 release. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (2): Last reviewed commit: "fix: address review - guard disposal sna..." | Re-trigger Greptile |
…lt collection The single-use guard now re-reads the disposed flag after claiming execution (the Interlocked.Exchange is a full fence), so a disposal that completed before the ExecuteAsync call is always seen instead of a stale pre-claim snapshot. ProcessBatch's fault collection is inlined without the dead wasCanceled local: the catch filter already routes pure cancellation to the caller's OperationCanceledException handler, which completes the execution task as canceled. A regression test pins that contract.
This was referenced Jul 21, 2026
Closed
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
Resolves all eight pre-4.0 audit findings (#362–#369) in one change set, ahead of the v4.0 release freezing these contracts.
ExecuteAsync()on theIAsyncEnumerablevoid processors now returns aTaskCompletionSource-backed task, soTask.Exception.InnerExceptionscarries every failure (exactTask.WhenAllparity with the enumerable processors) whileawaitstill throws the first. Applied to the worker pool, parallel, batch, and one-at-a-time processors via a new internalStreamingExecutionhelper.ProcessInParallelextension drains and observes started tasks on mid-enumeration failure, then rethrows the source exception, instead of abandoning them fire-and-forget.GetResultsAsyncEnumerable()yield completion order;GetResultsAsync()yields source order. XML docs plus a new README "Result ordering" section.WithCancellationenumerations in theSelectMany/SelectManyAsynciterator families now useConfigureAwait(false).OperationCanceledExceptionfromWaitAsync/GetResultsAsync, instead ofArgumentExceptionnaming an internal constructor parameter. The cancellation registration inProcessorLifecycle.Startfires synchronously on the fully built instance, so the original partially-built-instance concern does not apply.CancelAllCorecatchesObjectDisposedExceptionaround the CTS cancel, matching the streaming processors' existingCancelForDisposalhandling.ExecuteAsynccall throwsInvalidOperationExceptioneagerly at the call site (result variants included — the guard is in a non-iterator wrapper); calling after disposal throwsObjectDisposedExceptionnaming the processor type.No public API signature changes — the PublicAPI analyzer is clean and the V3 binary-compatibility tests pass. The README migration guide gains five bullets covering the behaviour changes.
Test plan
PreCancelledTokenTests,SingleUseExecutionTests, andAsyncEnumerableExceptionFidelityTests(multi-failure fidelity, source-failure primacy, early-break cancellation for bounded and unbounded streams, extension drain)ValidationRegressionTests.Already_Cancelled_Token_Fails_Cleanly_At_Build_Timerewritten to the new contractTask.WhenEachfallback path)Fixes #362, #363, #364, #365, #366, #367, #368, #369.