Move the task completion source for a queue item into the queue item itself.#57617
Move the task completion source for a queue item into the queue item itself.#57617dibarbet merged 12 commits intodotnet:mainfrom
Conversation
| private static Task ExecuteCallbackAsync(QueueItem work, RequestContext? context, CancellationToken queueCancellationToken) | ||
| { | ||
| // Create a combined cancellation token to cancel any requests in progress when this shuts down | ||
| using var combinedTokenSource = queueCancellationToken.CombineWith(work.CancellationToken); |
There was a problem hiding this comment.
handled in the work.CallbackAsync method now - the queue item is responsible for the cancellation token from the client and the queue cancellation token is passed in. Before handling the request the tokens are combined and checked for cancellation
There was a problem hiding this comment.
note: an alternative formalizatino woudl be to not store hte combined token in the item, and instead have a queue of (IQueueItem, CancellationToken). They're isomorphic. But it's a question of who should morally own the CT.
| { | ||
| // Create a combined cancellation token so either the client cancelling it's token or the queue | ||
| // shutting down cancels the request. | ||
| using var combinedTokenSource = queueCancellationToken.CombineWith(queueCancellationToken); |
src/Features/LanguageServer/Protocol/Handler/RequestExecutionQueue.QueueItem.cs
Outdated
Show resolved
Hide resolved
| /// A task completion source representing the result of this queue item's work. | ||
| /// </summary> | ||
| public readonly Action<Exception> HandleQueueFailure; | ||
| public TaskCompletionSource<TResponseType?> CompletionSource { get; } |
There was a problem hiding this comment.
public? but not in the interface?
| internal partial class RequestExecutionQueue | ||
| { | ||
| private readonly struct QueueItem | ||
| private interface QueueItem |
There was a problem hiding this comment.
| private interface QueueItem | |
| private interface IQueueItem |
| public RequestMetrics Metrics { get; } | ||
| } | ||
|
|
||
| private readonly struct QueueItem<TRequestType, TResponseType> : QueueItem |
There was a problem hiding this comment.
being a struct isn't really helpful here if it's going to always be used through the itnerface.
| while (_queue.TryDequeue(out var item)) | ||
| { | ||
| _ = item.CallbackAsync(null, new CancellationToken(true)); | ||
| _ = item.TrySetCanceled(_cancelSource.Token); |
There was a problem hiding this comment.
something feels weird here with the CTs. it may be right. but i'm not feeling confident it it. I feel like we should not be cancelling it with our token, but shuld just be asking the item to cancel itself with it's own token that tracks its work.
--
note: i'd liek to actually talk through this so I can get a concreate understanding of exactly how cancellation works here. thanks :)
| /// Begins executing the work specified by this queue item. | ||
| /// </summary> | ||
| private readonly Func<RequestContext?, CancellationToken, Task> _callbackAsync; | ||
| public Task CallbackAsync(RequestContext? context, CancellationToken cancellationToken); |
There was a problem hiding this comment.
All of these access modifiers should probably be removed
| public Task CallbackAsync(RequestContext? context, CancellationToken cancellationToken); | |
| Task CallbackAsync(RequestContext? context, CancellationToken cancellationToken); |
There was a problem hiding this comment.
remind me. when do we call the callback when RequestContext is null? (feel free to doc this as well).
| } | ||
|
|
||
| return completion.Task; | ||
| return item.CompletionSource.Task; |
There was a problem hiding this comment.
❓ Does this execute inside Visual Studio? Is it expected to cooperate with JTF?
There was a problem hiding this comment.
it does execute in VS, however I believe it shouldn't need jtf since
- JTF does not keep context across the streamjsonrpc boundary, so even if the client is blocking the UI, using JTF here won't help
- LSP handlers should not be using the UI thread. If they do it is a bug. There currently one that does, but because of 1) it is broken in any scenario where the LSP server is running on the
same machineprocess ([LSP] CSharpAddMissingReference.InvokeSomeFixesInCSharpThenVerifyReferences can deadlock in LSP editor #51951)
|
You could probably avoid the interface by making the |
Wouldn't it still need to be generic since the |
3309450 to
43d6dc8
Compare
43d6dc8 to
52e5570
Compare
|
|
||
| public bool IsComplete() => _queue._queue.IsCompleted && _queue._queue.IsEmpty; | ||
|
|
||
| public async Task<bool> AreAllItemsCancelledAsync() |
There was a problem hiding this comment.
this function scares me a bit. it asks a query question, but it mutates to process.
| while (!_queue._queue.IsEmpty) | ||
| { | ||
| var item = await _queue._queue.DequeueAsync().ConfigureAwait(false); | ||
| if (!item.CombinedCancellationToken.IsCancellationRequested) |
There was a problem hiding this comment.
if this is false, then the item was not canceled, but it was dequeued from teh queue... seems really strange. doc heavily if this is actualy what we want.
There was a problem hiding this comment.
didn't quite get this one actually - but this was changed to cancellationToken.ThrowIfCancellationRequested(); This just exits early if the work item was cancelled while in queue so we don't even try to run it.
src/Features/LanguageServer/Protocol/Handler/RequestExecutionQueue.QueueItem.cs
Outdated
Show resolved
Hide resolved
| _logger.TraceInformation($"{MethodName} - Canceled while in queue"); | ||
| this.Metrics.RecordCancellation(); | ||
|
|
||
| _completionSource.SetCanceled(); |
There was a problem hiding this comment.
pass in the CT into SetCanceled?
There was a problem hiding this comment.
modified this bit to just ThrowIfCancellationRequested and let the catch below handle setting the result
| // 'null' response. Note: the lsp spec was checked to ensure that 'null' is valid for all | ||
| // the requests this could happen for. However, this assumption may not hold in the future. | ||
| // If that turns out to be the case, we could defer to the individual handler to decide | ||
| // what to do. |
There was a problem hiding this comment.
haha I believe you wrote it :)
| _completionSource.SetException(ex); | ||
|
|
||
| // Also allow the exception to flow back to the request queue to handle as appropriate | ||
| throw new InvalidOperationException($"Error handling '{MethodName}' request: {ex.Message}", ex); |
There was a problem hiding this comment.
this is def a bit... interesting. not saying it's wrong. but i always find ti a bit suspect . it would be good to have a deeper comment on why this is needed. for example, since we did _completionSource.SetException(ex); this somewhat implies that someone is not observing the TCS itself (but still is observing the direction exception thrown).
I wouldn't mind an actual case listed where that happens. note: i get this is a move, so we should keep even if you can't give a case.
There was a problem hiding this comment.
This part is fairly important.
The _completionSource represents the task completion as the LSP client sees it, so we of course send the exception result back to the client.
However, that completionSource doesn't represent the result of the call inside the queue processing. By throwig the exception here as well, the queue processing in ProcessQueueAsync also sees the exception and decides (depending on if the request was mutating or not) how to handle the exception (in the case of mutating, it shuts down the server, for non-mutating it just reports a non-fatal).
src/Features/LanguageServer/Protocol/Handler/RequestExecutionQueue.QueueItem.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/RequestExecutionQueue.cs
Outdated
Show resolved
Hide resolved
| private static Task ExecuteCallbackAsync(QueueItem work, RequestContext? context, CancellationToken queueCancellationToken) | ||
| { | ||
| // Create a combined cancellation token to cancel any requests in progress when this shuts down | ||
| using var combinedTokenSource = queueCancellationToken.CombineWith(work.CancellationToken); |
There was a problem hiding this comment.
note: an alternative formalizatino woudl be to not store hte combined token in the item, and instead have a queue of (IQueueItem, CancellationToken). They're isomorphic. But it's a question of who should morally own the CT.
src/Features/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/RequestExecutionQueue.DocumentChangeTracker.cs
Show resolved
Hide resolved
…nc return the result of the TCS
bc50762 to
ff7dee1
Compare
| { | ||
| _completionSource = new TaskCompletionSource<TResponseType?>(); | ||
| // Set the tcs state to cancelled if the token gets cancelled outside of our callback (for example the server shutting down). | ||
| cancellationToken.Register(() => _completionSource.TrySetCanceled(cancellationToken)); |
There was a problem hiding this comment.
@CyrusNajmabadi this was a bit I wasn't sure if it was correct. But basically if an exception happens before we actually start executing the work (e.g. an exception in creating the request context), then we cancel the queue token and shutdown the server.
However, the test (or client) would still be waiting on the result of the TCS which never transitions to the canceled state since the CallbackAsync method never executes. So I register an action on cancellation of the combined token to actually cancel the TCS.
src/Features/LanguageServer/Protocol/Handler/RequestExecutionQueue.QueueItem.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/RequestExecutionQueue.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/RequestExecutionQueue.cs
Outdated
Show resolved
Hide resolved
| // The queue's cancellation token was invoked which means we are shutting down the queue. | ||
| // Exit out of the loop so we stop processing new items. | ||
| return; | ||
| } |
There was a problem hiding this comment.
note: if you don't like this try/catch, i believe @genlu is adding an extension to await this without throign cancellation. You can then just await, and tehn do a quick check/bail if the _cancelSource is canceled.
| // Log it, shutdown the queue, and exit the loop. | ||
| _logger.TraceException(ex); | ||
| OnRequestServerShutdown($"Error occurred processing queue in {_serverName}: {ex.Message}."); | ||
| return; |
There was a problem hiding this comment.
nice. so it worked to not have to tell the item to handle this?
Resolves #57221