Skip to content

Avoid cancellation exceptions in intermediate operations#55138

Merged
sharwell merged 6 commits intodotnet:mainfrom
sharwell:fewer-exceptions-2
Jul 27, 2021
Merged

Avoid cancellation exceptions in intermediate operations#55138
sharwell merged 6 commits intodotnet:mainfrom
sharwell:fewer-exceptions-2

Conversation

@sharwell
Copy link
Copy Markdown
Contributor

Addresses the following exceptions:

image

@sharwell sharwell requested a review from a team as a code owner July 27, 2021 15:51
@ghost ghost added the Area-IDE label Jul 27, 2021
@sharwell sharwell marked this pull request as draft July 27, 2021 15:52
else
{
return new SerializableSourceText(await state.GetTextAsync(cancellationToken).ConfigureAwait(false));
return SpecializedTasks.TransformWithoutIntermediateCancellationExceptionAsync(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it woudl be good to have a comment here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intent is to have the method name eliminate the need for a separate comment. Open to suggestions for improving upon the current name.

// Synchronous fast path if 'func' completes synchronously
var result = intermediateResult.Result;
if (cancellationToken.IsCancellationRequested)
return new ValueTask<TResult>(Task.FromCanceled<TResult>(cancellationToken));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no ValueTask constructor to cancel it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think so? Will double check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ There is not.

// | Direct fault¹ | Not cancelled | Fault result without applying transform |
// | Direct fault¹ | Cancelled | Cancel result without applying transform |
// | Indirect fault | Not cancelled | Fault result without applying transform |
// | Indirect fault | Cancelled | Cancel result without applying transform |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what'st eh difference of Direct vs Indirect fault?

Copy link
Copy Markdown
Contributor Author

@sharwell sharwell Jul 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct fault means func throws its exception directly (a non-async implementation that throws). Indirect fault means func completes to the point of returning a ValueTask<TIntermediate> instance which (immediately or eventually) transitions to the faulted state. Methods marked async always result in indirect faults.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha. can you add that comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Expanded comment to cover this case

// | Ran to completion | Not cancelled | Apply transform |
// | Ran to completion | Cancelled | Cancel result without applying transform |
// | Cancelled (matching token) | Cancelled | Cancel result without applying transform |
// | Cancelled (mismatch token) | Not cancelled | Cancel result without applying transform |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is the most interesting. woudl that indicate a bug?

Copy link
Copy Markdown
Contributor Author

@sharwell sharwell Jul 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ This is almost the same behavior you have for normal async/await. The only difference is this helper will produce a result task with the input cancellation token to the method instead of using the one included in the nested cancellation.

We could get an exact match, but it would require changing the implementation of UnwrapAndTransformAsync to use the asynchronous form of ContinueWith followed by Unwrap, which results in a performance penalty for the common case.

// | Cancelled (mismatch token) | Not cancelled | Cancel result without applying transform |
// | Cancelled (mismatch token) | Cancelled | Cancel result without applying transform |
// | Direct fault¹ | Not cancelled | Fault result without applying transform |
// | Direct fault¹ | Cancelled | Cancel result without applying transform |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems fine. but it feels a bit lossy. is cancellation preferred here over bubbling the fault up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing through the fault is heavily preferred, but we can't do that without adding a performance cost to the common cases. See #55138 (comment).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗ This documentation is not correct. These are the results for indirect faults. I will fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK now the table is correct for these two. The unfortunate case is the final case (indirect fault combined with a cancellation request).


gate.Set();
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => task.AsTask());
Assert.Same(fault, exception);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i skimmed. but i assume you're testing the table below?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was my attempt to cover the full table, including synchronous and asynchronous forms of each possibility of intermediateResult.

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

This looks great to me. Also tagging @stephentoub for any thoughts he has, or ideas on patterns that maybe TPL coudl adopt to help out here.

@sharwell sharwell force-pushed the fewer-exceptions-2 branch from 8a976c0 to 20decaa Compare July 27, 2021 19:17
@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

still looks great to me. thanks for the corrections and clarifications.

@sharwell sharwell force-pushed the fewer-exceptions-2 branch from 20decaa to 0d079d4 Compare July 27, 2021 19:32
@sharwell sharwell marked this pull request as ready for review July 27, 2021 19:40
@sharwell sharwell enabled auto-merge July 27, 2021 21:36
@sharwell sharwell merged commit 719d4ff into dotnet:main Jul 27, 2021
@ghost ghost added this to the Next milestone Jul 27, 2021
@sharwell sharwell deleted the fewer-exceptions-2 branch July 27, 2021 21:36
@allisonchou allisonchou modified the milestones: Next, 17.0.P3 Jul 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants