Skip to content

fix: resolve pre-4.0 audit findings across streaming and cancellation paths#370

Merged
thomhurst merged 2 commits into
mainfrom
fix/pre-4-audit-fixes
Jul 21, 2026
Merged

fix: resolve pre-4.0 audit findings across streaming and cancellation paths#370
thomhurst merged 2 commits into
mainfrom
fix/pre-4-audit-fixes

Conversation

@thomhurst

Copy link
Copy Markdown
Owner

Summary

Resolves all eight pre-4.0 audit findings (#362#369) in one change set, ahead of the v4.0 release freezing these contracts.

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

  • 14 new tests across PreCancelledTokenTests, SingleUseExecutionTests, and AsyncEnumerableExceptionFidelityTests (multi-failure fidelity, source-failure primacy, early-break cancellation for bounded and unbounded streams, extension drain)
  • ValidationRegressionTests.Already_Cancelled_Token_Fails_Cleanly_At_Build_Time rewritten to the new contract
  • Full suite: 572 tests × net8.0 / net9.0 / net10.0, all passing (net8.0 exercises the Task.WhenEach fallback path)

Fixes #362, #363, #364, #365, #366, #367, #368, #369.

… 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-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR updates streaming execution and cancellation behavior before the v4 release. The main changes are:

  • Preserves multiple asynchronous failures on returned tasks.
  • Cancels and drains work when result streams are abandoned.
  • Enforces single-use execution across streaming processors.
  • Handles pre-cancelled tokens and disposal races.
  • Documents result ordering and migration behavior.

Confidence Score: 5/5

This looks safe to merge.

  • Purely cancelled batches now complete in the cancelled state.
  • The execution guard reads disposal state after claiming execution.
  • No blocking follow-up issue remains in the reviewed fixes.

Important Files Changed

Filename Overview
EnumerableAsyncProcessor/RunnableProcessors/AsyncEnumerable/AsyncEnumerableBatchProcessor.cs Propagates pure batch cancellation and preserves all failures from faulted batches.
EnumerableAsyncProcessor/StreamingExecution.cs Adds shared single-use guards, completion handling, and abandoned-task observation.
EnumerableAsyncProcessor/AsyncEnumerableWorkerPool.cs Preserves worker failures and bounds cleanup when result enumeration ends early.
EnumerableAsyncProcessor/ProcessorLifecycle.cs Supports pre-cancelled tokens and handles concurrent cancellation-source disposal.

Reviews (2): Last reviewed commit: "fix: address review - guard disposal sna..." | Re-trigger Greptile

Comment thread EnumerableAsyncProcessor/StreamingExecution.cs Outdated
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Async-enumerable void path surfaces only the first exception; remaining failures are unrecoverable

1 participant