Skip to content

Move determination of language for an LSP request into the serialized queue to avoid race#74300

Merged
dibarbet merged 5 commits intodotnet:mainfrom
dibarbet:fix_lsp_language_2
Jul 10, 2024
Merged

Move determination of language for an LSP request into the serialized queue to avoid race#74300
dibarbet merged 5 commits intodotnet:mainfrom
dibarbet:fix_lsp_language_2

Conversation

@dibarbet
Copy link
Copy Markdown
Member

@dibarbet dibarbet commented Jul 9, 2024

Resolves dotnet/vscode-csharp#6973
Resolves dotnet/vscode-csharp#7303

Previously we were doing deserialization and language determination in the direct StreamJsonRpc callback, before the request got added to the queue.

This caused a race when the language was provided as part of a DidOpen - the language would get saved as part of running the DidOpen request in the queue, however it is possible (likely even) that the next request hit the StreamJsonRpc callback and attempted to retrieve the language (outside of the queue) before the queue could run the prior DidOpen.

Additionally, any deserialization failures would not be observed by our exception reporting (as it also happened outside the queue).

This modifies the queue implementation to instead do all deserialization and language determination inside the serial execution part of the queue. This fixes the race as it ensures the previous DidOpen completes before the next request attempts to determine the language.

Additionally, serialization failures are now tracked. Failures in mutating requests will shut down the server, non-mutating will not.

@ghost ghost added Area-Infrastructure untriaged Issues and PRs which have not yet been triaged by a lead labels Jul 9, 2024
@dibarbet dibarbet force-pushed the fix_lsp_language_2 branch from 6ce37ea to 50fe512 Compare July 9, 2024 07:03
@dibarbet dibarbet force-pushed the fix_lsp_language_2 branch from 50fe512 to fdaf58d Compare July 9, 2024 18:30
{
_method = method;
_typeRefResolver = typeRefResolver;
_languageEntryPoint = new Lazy<FrozenDictionary<string, (MethodInfo, RequestHandlerMetadata)>>(() =>
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 code generally moved into the RequestExecutionQueue

await task.ConfigureAwait(false);
var resultProperty = task.GetType().GetProperty("Result") ?? throw new InvalidOperationException("Result property on task cannot be null");
var result = resultProperty.GetValue(task);
var result = await queue.ExecuteAsync(requestObject, _method, lspServices, cancellationToken).ConfigureAwait(false);
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.

Deserialization and things moved into the queue, had to move the reflection invocation down into the queue as well

private readonly JsonSerializer _jsonSerializer = jsonSerializer;

protected override DelegatingEntryPoint CreateDelegatingEntryPoint(string method, IGrouping<string, RequestHandlerMetadata> handlersForMethod)
public override TRequest DeserializeRequest<TRequest>(object? serializedRequest, RequestHandlerMetadata metadata)
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.

generally just moved from further down in this file.

return context;
}

public TRequest? TryDeserializeRequest<TRequest>(AbstractLanguageServer<TRequestContext> languageServer, RequestHandlerMetadata requestHandlerMetadata, bool isMutating)
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.

Previously, deserialization errors would never be observed by the queue, the request would just fail (but the server would continue functioning).

Now that we're deserializing in the queue, we log the errors and throw/catch based on what kind of request it is.

/// The handler info is created lazily to avoid instantiating any types or handlers until a request is recieved for
/// that particular method and language.
/// </summary>
private readonly FrozenDictionary<string, FrozenDictionary<string, Lazy<(RequestHandlerMetadata Metadata, IMethodHandler Handler, MethodInfo MethodInfo)>>> _handlerInfoMap;
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.

Caching method infos for invoking handlers with specific parameters.

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.

nit: i recommend the Rewrap extension, set to 120 column. Makes it easy to wrap comments so you don't have to mnually fix things up.

var languages = new Dictionary<string, Lazy<(RequestHandlerMetadata, IMethodHandler, MethodInfo)>>();
foreach (var metadata in methodGroup)
{
languages.Add(metadata.Language, new(() =>
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 was migrated from AbstractLanguageServer


// The request context must be created serially inside the queue to so that requests always run
// on the correct snapshot as of the last request.
var context = await work.CreateRequestContextAsync(deserializedRequest, handler, cancellationToken).ConfigureAwait(false);
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.

Everything below this was just moved into this method.

private readonly JsonSerializerOptions _jsonSerializerOptions = options;

protected override DelegatingEntryPoint CreateDelegatingEntryPoint(string method, IGrouping<string, RequestHandlerMetadata> handlersForMethod)
public override TRequest DeserializeRequest<TRequest>(object? serializedRequest, RequestHandlerMetadata metadata)
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.

moved from further down in the file

protected internal override void BeforeRequest<TRequest>(TRequest request)
{
// Update the locale for this request to the desired LSP locale.
CultureInfo.CurrentUICulture = GetCultureForRequest();
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.

Fixing a bug that showed up after the queue changes - previously we were running this in WrapStartRequestTaskAsync, but the task could potentially be completed by the time WrapStartRequestTaskAsync was called. Instead we need to explicitly set this before the request task runs.

@dibarbet dibarbet marked this pull request as ready for review July 9, 2024 18:49
@dibarbet dibarbet requested a review from a team as a code owner July 9, 2024 18:49
var lspServices = target.GetLspServices();

// Retrieve the language of the request so we know how to deserialize it.
var language = target.GetLanguageForRequest(_method, request);
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 was the problematic part. getting the language outside of the queue.

}
else
{
return default;
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.

ah, this answers teh prior question. i'd like if the prior docs can explain this.

_logger.LogException(ex);

// Set the task result to the exception
_completionSource.TrySetException(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.

interesting htat we set the exception like this in the failure case. but the above try doesn't do TrySetResult in the success case.

Also... this code will run in the case of cancellation. Is that desirable? Should cancellation be handled in this exact same way. If so, can you add a comment to the effect that we do want that.

note: TrySetException on cancellation is odd. I would expect TrySetCanceled.

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.

The try doesn't set a result because it needs to do more processing to set the actual result (invoke the handler).

However if we encounter an exception, we can't continue processing so we set the result on the source so the client can observe it (the request is failed).

Also... this code will run in the case of cancellation. Is that desirable? Should cancellation be handled in this exact same way. If so, can you add a comment to the effect that we do want that.

Good question. If there is no cancellation token passed here do we need to handle that?

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.

Good question. If there is no cancellation token passed here do we need to handle that?

probably not. but it would be good to then add asserst taht we do NOT expect to get cancellation exceptions and whatnot.

it's a little unclear to me how this all flows. For example, some people pass the CT along as a property of some context-object. I don't like that pattern because then i can't see if cancellation flows in. But i really don't have a good sense of what is going on here.

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.

The try doesn't set a result because it needs to do more processing to set the actual result (invoke the handler).

doc the code :)

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.

some more comments on this. will share in person though.

/// Reflection invokes <see cref="ProcessQueueCoreAsync{TRequest, TResponse}(IQueueItem{TRequestContext}, IMethodHandler, RequestHandlerMetadata, ConcurrentDictionary{Task, CancellationTokenSource}, CancellationTokenSource?, CancellationToken)"/>
/// using the concrete types defined by the handler's metadata.
/// </summary>
[Obsolete("Only for use by legacy XAML LSP")]
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.

wacky.

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

Overall have no issue with this. Have some questions and tiny requests to cover before i sign off. but the approach makes sense :)

@dibarbet dibarbet merged commit 7e28266 into dotnet:main Jul 10, 2024
@dibarbet dibarbet deleted the fix_lsp_language_2 branch July 10, 2024 06:33
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Jul 10, 2024
@RikkiGibson RikkiGibson modified the milestones: Next, 17.12 P1 Jul 29, 2024
ryzngard pushed a commit to dotnet/razor that referenced this pull request Oct 22, 2024
Mirrors dotnet/roslyn#74300 for razor

fixes devdiv.visualstudio.com/DevDiv/_workitems/edit/2286513?src=WorkItemMention&src-action=artifact_link
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Infrastructure untriaged Issues and PRs which have not yet been triaged by a lead

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C# request textdocument/inlayhint failed. Multiple Errors Appear When Creating a C# Text File

3 participants