Skip to content

Move the task completion source for a queue item into the queue item itself.#57617

Merged
dibarbet merged 12 commits intodotnet:mainfrom
dibarbet:lsp_queue_task
Nov 16, 2021
Merged

Move the task completion source for a queue item into the queue item itself.#57617
dibarbet merged 12 commits intodotnet:mainfrom
dibarbet:lsp_queue_task

Conversation

@dibarbet
Copy link
Copy Markdown
Member

@dibarbet dibarbet commented Nov 6, 2021

Resolves #57221

@dibarbet dibarbet added Area-IDE LSP issues related to the roslyn language server protocol implementation labels Nov 6, 2021
@dibarbet dibarbet requested a review from a team as a code owner November 6, 2021 02:56
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);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

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.

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);
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.

wat?

/// A task completion source representing the result of this queue item's work.
/// </summary>
public readonly Action<Exception> HandleQueueFailure;
public TaskCompletionSource<TResponseType?> CompletionSource { get; }
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.

public? but not in the interface?

internal partial class RequestExecutionQueue
{
private readonly struct QueueItem
private interface QueueItem
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.

Suggested change
private interface QueueItem
private interface IQueueItem

public RequestMetrics Metrics { get; }
}

private readonly struct QueueItem<TRequestType, TResponseType> : QueueItem
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.

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);
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.

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);
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.

All of these access modifiers should probably be removed

Suggested change
public Task CallbackAsync(RequestContext? context, CancellationToken cancellationToken);
Task CallbackAsync(RequestContext? context, CancellationToken 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.

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;
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.

❓ Does this execute inside Visual Studio? Is it expected to cooperate with JTF?

Copy link
Copy Markdown
Member Author

@dibarbet dibarbet Nov 8, 2021

Choose a reason for hiding this comment

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

it does execute in VS, however I believe it shouldn't need jtf since

  1. JTF does not keep context across the streamjsonrpc boundary, so even if the client is blocking the UI, using JTF here won't help
  2. 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 machine process ([LSP] CSharpAddMissingReference.InvokeSomeFixesInCSharpThenVerifyReferences can deadlock in LSP editor #51951)

@sharwell
Copy link
Copy Markdown
Contributor

sharwell commented Nov 8, 2021

You could probably avoid the interface by making the QueueItem constructor private and adding a generic Create<TResponse> method where callbackAsync returns Task<TResult> instead of just Task.

@dibarbet
Copy link
Copy Markdown
Member Author

dibarbet commented Nov 8, 2021

You could probably avoid the interface by making the QueueItem constructor private and adding a generic Create<TResponse> method where callbackAsync returns Task<TResult> instead of just Task.

Wouldn't it still need to be generic since the TaskCompletionSouce requires a type param? I could make it a TaskCompletionSource<object> perhaps, but I'm not sure if I prefer that over just the interface


public bool IsComplete() => _queue._queue.IsCompleted && _queue._queue.IsEmpty;

public async Task<bool> AreAllItemsCancelledAsync()
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 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)
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.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

_logger.TraceInformation($"{MethodName} - Canceled while in queue");
this.Metrics.RecordCancellation();

_completionSource.SetCanceled();
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.

pass in the CT into SetCanceled?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.
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.

great comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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);
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 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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).

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);
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.

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.

{
_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));
Copy link
Copy Markdown
Member Author

@dibarbet dibarbet Nov 9, 2021

Choose a reason for hiding this comment

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

@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.

// 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;
}
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.

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;
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.

nice. so it worked to not have to tell the item to handle this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yup, discussed #57617 (comment) offline

@dibarbet dibarbet merged commit ef81088 into dotnet:main Nov 16, 2021
@ghost ghost added this to the Next milestone Nov 16, 2021
@dibarbet dibarbet deleted the lsp_queue_task branch November 16, 2021 00:06
allisonchou added a commit to allisonchou/roslyn that referenced this pull request Nov 17, 2021
@allisonchou allisonchou modified the milestones: Next, 17.1.P2 Nov 30, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-IDE LSP issues related to the roslyn language server protocol implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[LSP] Investigate moving taskcompletionsource to queue item instead of callbacks

4 participants