-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Avoid cancellation exceptions in async queue implementation #68412
Conversation
Can we instead wait for @jcouv 's work to be done. This code is pretty confusing to understand. |
src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/TestHooks/ValueTaskExtensions.cs
Outdated
Show resolved
Hide resolved
// If any of the requests was high priority, then compute at that speed. | ||
var highPriority = changes.Contains(true); | ||
await RecomputeTagsAsync(highPriority, cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 This await
resulted in 47916 exceptions in the trace I'm reviewing. Following this change, there are no exceptions thrown from this location.
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs
Show resolved
Hide resolved
// tagging operation. Otherwise, it is impossible to know which changes occurred prior to the | ||
// request to tag, and which ones occurred during the tagging itself. | ||
this.AccumulatedTextChanges = null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm not getting the logic here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is a separate commit. It might help to review it that way.
The underlying problem is we claimed data from the type and reset that data at the point of acquisition. If cancellation occurs after acquisition but before processing completes, the accumulated text changes for that batch processing operation were simply discarded previously. The new code will re-process the accumulated text changes if cancellation interrupts processing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ I retained this change as it is a correctness fix
src/Workspaces/Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue`1.cs
Outdated
Show resolved
Hide resolved
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very worried about the difficulty validating that logic like this is correct. I would prefer we just wait on the compiler doing this work for us so we can get the battle testing of that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be happy to have @stephentoub review this. I would consider it a candidate for inclusion in vs-threading along with the NoThrowAwaitable
method they already have there for Task<TResult>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I'm not a fan of changing the await to return a tuple of the status, exception, and result and would hope we wouldn't promote that. We're adding support for no-throw awaiting in .NET 8 and, after many go-arounds on design and discussing doing this exact thing and various variations, are very explicitly only doing so for Task and not ValueTask. If you really need to do this with ValueTask, you have various options, like
vt = vt.Preserve();
await vt.NoThrow(); // your simple awaiter that just nops GetResult
... examine vt
and it doesn't require inventing a complicated thing or using an unfamiliar pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⏱️ Works for me. I'll rewrite to this pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ This has now been rewritten.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ This code will be rewritten to use microsoft/vs-threading#1193 after 17.7 Preview 3 ships.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was this done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ No, I checked CI this morning and the images are still on 17.7 Preview 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently blocking. but open to discussion on this. but it should be combined with the convo on compiler async/await emit.
/// </summary> | ||
public (TaskStatus Status, ExceptionDispatchInfo? Exception, TResult? Result) GetResult() | ||
{ | ||
var preserved = _task.Preserve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this VT is backed by an IValueTaskSource and it's faulted or canceled, this is going to result in throwing and catching its exception. My understanding is that's the cost this whole thing is trying to prevent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also on very shaky ground. It may work most of the time, but again with an IVTS, this is calling Preserve after it's already hooked up a continuation to the IVTS. That's not something that's technically supported, and could possibly fail with some implementations. I think it should work with ours. But, shaky ground.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ This has been rewritten to call Preserve()
at the point the awaitable is initialized.
My understanding is that's the cost this whole thing is trying to prevent.
This isn't the specific situation impacting us, so it's acceptable that there is no clear resolution for it.
// no point preceding if we're already disposed. We check this on the UI thread so that we will know | ||
// about any prior disposal on the UI thread. | ||
if (cancellationToken.IsCancellationRequested) | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 I retained this change as it served no purpose (SwitchToMainThreadAsync
checks the cancellation token immediately before this).
if (cancellationToken.IsCancellationRequested) | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 I retained these changes as they represent a trivial consistency update to this method, and do not overlap with the planned compiler changes.
// tagging operation. Otherwise, it is impossible to know which changes occurred prior to the | ||
// request to tag, and which ones occurred during the tagging itself. | ||
this.AccumulatedTextChanges = null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ I retained this change as it is a correctness fix
Changes which overlap the compiler work have been removed from this pull request
var batchResultTask = _processBatchAsync(nextBatch, batchCancellationToken).Preserve(); | ||
await batchResultTask.NoThrowAwaitableInternal(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 This change is retained because it does not overlap with compiler work. The no-throw logic in async state machines does not apply to await
statements within a try
block that contains one or more catch
clauses, so avoiding an exception here requires updating to a no-throw form of awaiter.
@@ -170,32 +170,31 @@ private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) | |||
private void EnqueueWork(bool highPriority) | |||
=> _eventChangeQueue.AddWork(highPriority, _dataSource.CancelOnNewWork); | |||
|
|||
private async ValueTask ProcessEventChangeAsync(ImmutableSegmentedList<bool> changes, CancellationToken cancellationToken) | |||
private ValueTask<VoidResult> ProcessEventChangeAsync(ImmutableSegmentedList<bool> changes, CancellationToken cancellationToken) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the urpse of hte 'Voidresult' in all of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Required as part of the move to AsyncBatchingWorkQueue<bool, VoidResult>
. The helper type AsyncBatchingWorkQueue<bool>
is not suitable for this scenario due to performance impact in an edge case. This restriction is documented and localized to private implementation code.
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs
Show resolved
Hide resolved
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs
Show resolved
Hide resolved
else | ||
{ | ||
// Access the result to force the exception to be thrown. | ||
_ = batchResultTask.Result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this throw cancellation in the event of cancellatino? or does it throw an aggregate exception? should this be GetAwaiter().GetResult() instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ This is a ValueTask, so there is no need to use GetAwaiter().GetResult()
(exceptions are not wrapped in AggregateException)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you comment this. needing to know the subtle differences between the task systems is non trivial mental overhead for validating this stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Now uses helper method VerifyCompleted
public NoThrowValueTaskAwaitable(ValueTask task, bool captureContext) | ||
{ | ||
Contract.ThrowIfNull(task, nameof(task)); | ||
_task = task.Preserve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Rewrote this to use a direct copy of the code from vs-threading (only changes were renames to avoid code style warnings):
https://github.com/microsoft/vs-threading/blob/main/src/Microsoft.VisualStudio.Threading/TplExtensions.cs
public NoThrowValueTaskAwaitable(ValueTask<TResult> task, bool captureContext) | ||
{ | ||
Contract.ThrowIfNull(task, nameof(task)); | ||
_task = task.Preserve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Rewrote this to use a direct copy of the code from vs-threading (only changes were renames to avoid code style warnings):
https://github.com/microsoft/vs-threading/blob/main/src/Microsoft.VisualStudio.Threading/TplExtensions.cs
// tagging operation. Otherwise, it is impossible to know which changes occurred prior to the | ||
// request to tag, and which ones occurred during the tagging itself. | ||
this.AccumulatedTextChanges = null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. this change seems very safe to me. That said, i would prefer the comment on it (or some documentation somewhere) be expanded a bit. namely, stating that it's ok to not clear this out, as the only impact is slightly worse performance for some taggers by indicating potentially more of the document was changed than actually changed.
Basically, i'd like docs somewhere that this is more a conservative estimate of what changed, and it is ok to be more (but not less) than what actually did change. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Documentation updated
// Don't bubble up cancellation to the queue for the nested batch cancellation. Just because we decided | ||
// to cancel this batch isn't something that should stop processing further batches. | ||
return default; | ||
var batchResultTask = _processBatchAsync(nextBatch, batchCancellationToken).Preserve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. just to check. we need to all .Preserve because not only are we awaiting this value-task, but we're also doing things like directly checking .Result (and other members) off of it. And ValueTasks otherwise do not support this after an await
because they may be put back in a pool?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Correct
return task.GetAwaiter().GetResult(); | ||
// Propagate any exceptions that may have been thrown. Relies on ValueTask.Result behaving like | ||
// Task.GetAwaiter().GetResult() in the presence of exceptions. | ||
return task.Result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't get it. why was the old code wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ The old code had the same behavior for the case where the ValueTask<T>
is already completed, but @stephentoub has suggested on some other cases where I used .GetAwaiter().GetResult()
that it was unnecessary and we can just use .Result
for ValueTask<T>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ I removed this change from the pull request.
Avoids an intermediate async state machine that throws a frequent cancellation exception.
internal static class ValueTaskExtensions | ||
{ | ||
// Following code is copied from Microsoft.VisualStudio.Threading.TplExtensions (renamed to avoid ambiguity) | ||
// https://github.com/microsoft/vs-threading/blob/main/src/Microsoft.VisualStudio.Threading/TplExtensions.cs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this file up to date with the code in vs-threading? (i just want to make sure if we have a copy, we're reasonably close to it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Yes, the only changes are renamed private fields to have _
prefix, and removal of this.
qualification.
* Avoid throwing cancellation exceptions (follow-up to dotnet#68412) * Use provided NoThrowAwaitable extension method
Addresses 59+% of the exceptions (220K out of 375K total) observed in CABs associated with AB#1827592.