Found in the pre-4.0 audit (follow-up to #360/#361).
ProcessorLifecycle's constructor calls ValidationHelper.ValidateCancellationTokenSource, which throws ArgumentException("'cancellationTokenSource' has already been cancelled.") when the linked source is already cancelled. The parameter it names is an internal constructor parameter the user never passed.
Failure scenario: items.ForEachAsync(f, alreadyCancelledToken).ProcessInParallel() throws ArgumentException from the terminal builder call. TPL convention for a pre-cancelled token is a canceled task, not an argument error.
Inconsistency — three behaviors for the same input:
- Builder path (enumerable source):
ArgumentException at the terminal call
ParallelExtensions.InParallelAsync: returns a canceled task
- Async-enumerable streaming path: no validation;
OperationCanceledException when ExecuteAsync is awaited
Race: a token cancelling between builder creation and the terminal call makes the observed exception type nondeterministic. No test covers any of this.
Proposal: drop the already-cancelled throw (keep the null check); let Start's registration fire immediately so every per-item completion source is canceled and WaitAsync/GetResultsAsync observe cancellation. The registration-in-Start design already makes this safe on a fully built instance. Contract change — decide before 4.0.
Found in the pre-4.0 audit (follow-up to #360/#361).
ProcessorLifecycle's constructor callsValidationHelper.ValidateCancellationTokenSource, which throwsArgumentException("'cancellationTokenSource' has already been cancelled.")when the linked source is already cancelled. The parameter it names is an internal constructor parameter the user never passed.Failure scenario:
items.ForEachAsync(f, alreadyCancelledToken).ProcessInParallel()throwsArgumentExceptionfrom the terminal builder call. TPL convention for a pre-cancelled token is a canceled task, not an argument error.Inconsistency — three behaviors for the same input:
ArgumentExceptionat the terminal callParallelExtensions.InParallelAsync: returns a canceled taskOperationCanceledExceptionwhenExecuteAsyncis awaitedRace: a token cancelling between builder creation and the terminal call makes the observed exception type nondeterministic. No test covers any of this.
Proposal: drop the already-cancelled throw (keep the null check); let
Start's registration fire immediately so every per-item completion source is canceled andWaitAsync/GetResultsAsyncobserve cancellation. The registration-in-Startdesign already makes this safe on a fully built instance. Contract change — decide before 4.0.