-
Notifications
You must be signed in to change notification settings - Fork 5.2k
[release/6.0-rc2] Fix corner-case handling of cancellation exception in ForEachAsync #59237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[release/6.0-rc2] Fix corner-case handling of cancellation exception in ForEachAsync #59237
Conversation
If code in Parallel.ForEachAsync throws OperationCanceledExceptions containing the CancellationToken passed to the iteration and that token has _not_ had cancellation requested (so why are they throwing with it) and there are no other exceptions, the ForEachAsync will effectively hang after failing to complete the task returned from it. The issue stems from how we treat cancellation. If the user-supplied token hasn't been canceled but we have OperationCanceledExceptions for the token passed into the iteration (the "internal" token), it can only have been canceled because an exception occurred. We filter out these cancellation exceptions, leaving just the exceptions that are deemed to have caused the failure in the first place. But the code doesn't currently account for the possibility that the developer is (arguably erroneously) throwing such an OperationCanceledException with the internal cancellation token as that root failure. The fix is to only filter out these OCEs if there are other exceptions besides them.
|
I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label. |
|
Tagging subscribers to this area: @dotnet/area-system-threading-tasks Issue DetailsBackport of #59065 to release/6.0-rc2 /cc @stephentoub Customer ImpactIf the body of a Parallel.ForEachAsync loop throws an OperationCanceledException using the supplied CancellationToken and it hasn't had cancellation requested, the Task returned from ForEachAsync will never complete and the calling code will effectively hang, e.g. await Parallel.ForEachAsync(source, async (item, cancellationToken) =>
{
...
throw new OperationCanceledException(cancellationToken); // when !cancellationToken.IsCancellationRequested
});This is not a common thing to do. However, if you do happen to do it, your app will hang. The code is filtering out such exceptions, but if they're the only ones, it ends up filtering out all of them, causing the call that would have completed the returned Task to throw an exception which is then inadvertently eaten. The fix is to change the design to simply not do such filtering: all exceptions thrown in such a case will be stored in the task. TestingAdditional unit tests. RiskRelatively low.
|
|
runtime (Mono Product Build windows x64 release) is 😞 |
I'm going to go ahead and assume my fix didn't cause that ;-) |
Backport of #59065 to release/6.0-rc2
/cc @stephentoub
Customer Impact
If the body of a Parallel.ForEachAsync loop throws an OperationCanceledException using the supplied CancellationToken and it hasn't had cancellation requested, the Task returned from ForEachAsync will never complete and the calling code will effectively hang, e.g.
This is not a common thing to do. However, if you do happen to do it, your app will hang. The code is filtering out such exceptions, but if they're the only ones, it ends up filtering out all of them, causing the call that would have completed the returned Task to throw an exception which is then inadvertently eaten.
The fix is to change the design to simply not do such filtering: all exceptions thrown in such a case will be stored in the task.
Testing
Additional unit tests.
Risk
Relatively low.
awaitthe returned task, which this won't affect, as await only throws the first exception stored in the task.