Skip to content
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

Cleanup LSP error reporting #74530

Merged
merged 1 commit into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,19 @@
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CommonLanguageServerProtocol.Framework;

namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript;

[ExportStatelessLspService(typeof(IRequestExecutionQueueProvider<RequestContext>), ProtocolConstants.TypeScriptLanguageContract), Shared]
internal sealed class VSTypeScriptRequestExecutionQueueProvider : IRequestExecutionQueueProvider<RequestContext>
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, true)]
internal sealed class VSTypeScriptRequestExecutionQueueProvider(IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider) : IRequestExecutionQueueProvider<RequestContext>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, true)]
public VSTypeScriptRequestExecutionQueueProvider()
{
}

public IRequestExecutionQueue<RequestContext> CreateRequestExecutionQueue(AbstractLanguageServer<RequestContext> languageServer, ILspLogger logger, AbstractHandlerProvider handlerProvider)
{
var queue = new RoslynRequestExecutionQueue(languageServer, logger, handlerProvider);
var queue = new RoslynRequestExecutionQueue(languageServer, logger, handlerProvider, asynchronousOperationListenerProvider);
queue.Start();
return queue;
}
Expand Down
10 changes: 9 additions & 1 deletion src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,15 @@ private ValueTask FilterLspTrackedDocumentsAsync(
{
if (documentUri is null || !trackedDocuments.ContainsKey(documentUri))
{
return notificationManager.SendRequestAsync(GetWorkspaceRefreshName(), cancellationToken);
try
{
return notificationManager.SendRequestAsync(GetWorkspaceRefreshName(), cancellationToken);
}
catch (StreamJsonRpc.ConnectionLostException)
{
// It is entirely possible that we're shutting down and the connection is lost while we're trying to send a notification
// as this runs outside of the guaranteed ordering in the queue. We can safely ignore this exception.
}
}
}

Expand Down
13 changes: 5 additions & 8 deletions src/LanguageServer/Protocol/RequestExecutionQueueProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,19 @@
using System.Composition;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CommonLanguageServerProtocol.Framework;

namespace Microsoft.CodeAnalysis.LanguageServer;

[ExportCSharpVisualBasicStatelessLspService(typeof(IRequestExecutionQueueProvider<RequestContext>)), Shared]
internal sealed class RequestExecutionQueueProvider : IRequestExecutionQueueProvider<RequestContext>
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, true)]
internal sealed class RequestExecutionQueueProvider(IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider) : IRequestExecutionQueueProvider<RequestContext>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, true)]
public RequestExecutionQueueProvider()
{
}

public IRequestExecutionQueue<RequestContext> CreateRequestExecutionQueue(AbstractLanguageServer<RequestContext> languageServer, ILspLogger logger, AbstractHandlerProvider handlerProvider)
{
var queue = new RoslynRequestExecutionQueue(languageServer, logger, handlerProvider);
var queue = new RoslynRequestExecutionQueue(languageServer, logger, handlerProvider, asynchronousOperationListenerProvider);
queue.Start();
return queue;
}
Expand Down
23 changes: 19 additions & 4 deletions src/LanguageServer/Protocol/RoslynRequestExecutionQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Roslyn.Utilities;

Expand All @@ -13,27 +16,39 @@ namespace Microsoft.CodeAnalysis.LanguageServer
internal sealed class RoslynRequestExecutionQueue : RequestExecutionQueue<RequestContext>
{
private readonly IInitializeManager _initializeManager;
private readonly IAsynchronousOperationListener _listener;

/// <summary>
/// Serial access is guaranteed by the queue.
/// </summary>
private CultureInfo? _cultureInfo;

public RoslynRequestExecutionQueue(AbstractLanguageServer<RequestContext> languageServer, ILspLogger logger, AbstractHandlerProvider handlerProvider)
public RoslynRequestExecutionQueue(AbstractLanguageServer<RequestContext> languageServer, ILspLogger logger, AbstractHandlerProvider handlerProvider, IAsynchronousOperationListenerProvider provider)
: base(languageServer, logger, handlerProvider)
{
_initializeManager = languageServer.GetLspServices().GetRequiredService<IInitializeManager>();
_listener = provider.GetListener(FeatureAttribute.LanguageServer);
}

public override Task WrapStartRequestTaskAsync(Task nonMutatingRequestTask, bool rethrowExceptions)
public override async Task WrapStartRequestTaskAsync(Task nonMutatingRequestTask, bool rethrowExceptions)
{
using var token = _listener.BeginAsyncOperation(nameof(WrapStartRequestTaskAsync));
if (rethrowExceptions)
{
return nonMutatingRequestTask;
try
Copy link
Member Author

Choose a reason for hiding this comment

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

fixes a bug here where we would only report NFW for non-mutating requests. If a mutating request failed we would never get fault telemetry for it.

{
await nonMutatingRequestTask.ConfigureAwait(false);
}
// If we had an exception, we want to record a NFW for it AND propogate it out to the queue so it can be handled appropriately.
catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, ErrorSeverity.Critical))
Copy link
Member Author

Choose a reason for hiding this comment

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

i believe this is the correct pattern to report and throw, but let me know if not.

{
throw ExceptionUtilities.Unreachable();
}
}
else
{
return nonMutatingRequestTask.ReportNonFatalErrorAsync();
// The caller has asked us to not rethrow, so record a NFW and swallow.
await nonMutatingRequestTask.ReportNonFatalErrorAsync().ConfigureAwait(false);
}
}

Expand Down
129 changes: 128 additions & 1 deletion src/LanguageServer/ProtocolUnitTests/HandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Roslyn.LanguageServer.Protocol;
Expand All @@ -32,7 +34,8 @@ public HandlerTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
typeof(TestNotificationHandlerFactory),
typeof(TestNotificationWithoutParamsHandlerFactory),
typeof(TestLanguageSpecificHandler),
typeof(TestLanguageSpecificHandlerWithDifferentParams));
typeof(TestLanguageSpecificHandlerWithDifferentParams),
typeof(TestConfigurableDocumentHandler));

[Theory, CombinatorialData]
public async Task CanExecuteRequestHandler(bool mutatingLspWorkspace)
Expand Down Expand Up @@ -135,6 +138,130 @@ public async Task ShutsdownIfDeserializationFailsOnMutatingRequest(bool mutating
await server.AssertServerShuttingDownAsync();
}

[Theory, CombinatorialData]
public async Task NonMutatingHandlerExceptionNFWIsReported(bool mutatingLspWorkspace)
{
await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace);

var request = new TestRequestWithDocument(new TextDocumentIdentifier
{
Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs")
});

var didReport = false;
FatalError.OverwriteHandler((exception, severity, dumps) =>
Copy link
Member Author

Choose a reason for hiding this comment

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

tests to verify NFW handler is called for mutating and non-mutating requests

{
if (exception.Message == nameof(HandlerTests) || exception.InnerException.Message == nameof(HandlerTests))
{
didReport = true;
}
});

var response = Task.FromException<TestConfigurableResponse>(new InvalidOperationException(nameof(HandlerTests)));
TestConfigurableDocumentHandler.ConfigureHandler(server, mutatesSolutionState: false, requiresLspSolution: true, response);

await Assert.ThrowsAnyAsync<Exception>(async ()
=> await server.ExecuteRequestAsync<TestRequestWithDocument, TestConfigurableResponse>(TestConfigurableDocumentHandler.MethodName, request, CancellationToken.None));

var provider = server.TestWorkspace.ExportProvider.GetExportedValue<AsynchronousOperationListenerProvider>();
await provider.WaitAllDispatcherOperationAndTasksAsync(
server.TestWorkspace,
FeatureAttribute.LanguageServer);

Assert.True(didReport);
}

[Theory, CombinatorialData]
public async Task MutatingHandlerExceptionNFWIsReported(bool mutatingLspWorkspace)
{
var server = await CreateTestLspServerAsync("", mutatingLspWorkspace);

var request = new TestRequestWithDocument(new TextDocumentIdentifier
{
Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs")
});

var didReport = false;
FatalError.OverwriteHandler((exception, severity, dumps) =>
{
if (exception.Message == nameof(HandlerTests) || exception.InnerException.Message == nameof(HandlerTests))
{
didReport = true;
}
});

var response = Task.FromException<TestConfigurableResponse>(new InvalidOperationException(nameof(HandlerTests)));
TestConfigurableDocumentHandler.ConfigureHandler(server, mutatesSolutionState: true, requiresLspSolution: true, response);

await Assert.ThrowsAnyAsync<Exception>(async ()
=> await server.ExecuteRequestAsync<TestRequestWithDocument, TestConfigurableResponse>(TestConfigurableDocumentHandler.MethodName, request, CancellationToken.None));

await server.AssertServerShuttingDownAsync();

Assert.True(didReport);
}

[Theory, CombinatorialData]
public async Task NonMutatingHandlerCancellationExceptionNFWIsNotReported(bool mutatingLspWorkspace)
{
await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace);

var request = new TestRequestWithDocument(new TextDocumentIdentifier
{
Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs")
});

var didReport = false;
FatalError.OverwriteHandler((exception, severity, dumps) =>
{
if (exception.Message == nameof(HandlerTests) || exception.InnerException.Message == nameof(HandlerTests))
{
didReport = true;
}
});

var response = Task.FromException<TestConfigurableResponse>(new OperationCanceledException(nameof(HandlerTests)));
TestConfigurableDocumentHandler.ConfigureHandler(server, mutatesSolutionState: false, requiresLspSolution: true, response);

await Assert.ThrowsAnyAsync<Exception>(async ()
=> await server.ExecuteRequestAsync<TestRequestWithDocument, TestConfigurableResponse>(TestConfigurableDocumentHandler.MethodName, request, CancellationToken.None));

var provider = server.TestWorkspace.ExportProvider.GetExportedValue<AsynchronousOperationListenerProvider>();
await provider.WaitAllDispatcherOperationAndTasksAsync(
server.TestWorkspace,
FeatureAttribute.LanguageServer);

Assert.False(didReport);
}

[Theory, CombinatorialData]
public async Task MutatingHandlerCancellationExceptionNFWIsNotReported(bool mutatingLspWorkspace)
{
await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace);

var request = new TestRequestWithDocument(new TextDocumentIdentifier
{
Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs")
});

var didReport = false;
FatalError.OverwriteHandler((exception, severity, dumps) =>
{
if (exception.Message == nameof(HandlerTests) || exception.InnerException.Message == nameof(HandlerTests))
{
didReport = true;
}
});

var response = Task.FromException<TestConfigurableResponse>(new OperationCanceledException(nameof(HandlerTests)));
TestConfigurableDocumentHandler.ConfigureHandler(server, mutatesSolutionState: true, requiresLspSolution: true, response);

await Assert.ThrowsAnyAsync<Exception>(async ()
=> await server.ExecuteRequestAsync<TestRequestWithDocument, TestConfigurableResponse>(TestConfigurableDocumentHandler.MethodName, request, CancellationToken.None));

Assert.False(didReport);
}

internal record TestRequestTypeOne([property: JsonPropertyName("textDocument"), JsonRequired] TextDocumentIdentifier TextDocumentIdentifier);

internal record TestRequestTypeTwo([property: JsonPropertyName("textDocument"), JsonRequired] TextDocumentIdentifier TextDocumentIdentifier);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Composition;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
using static Roslyn.Test.Utilities.AbstractLanguageServerProtocolTests;

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;

internal record TestRequestWithDocument([property: JsonPropertyName("textDocument"), JsonRequired] TextDocumentIdentifier TextDocumentIdentifier);

internal record TestConfigurableResponse([property: JsonPropertyName("response"), JsonRequired] string Response);

[ExportCSharpVisualBasicStatelessLspService(typeof(TestConfigurableDocumentHandler)), PartNotDiscoverable, Shared]
[LanguageServerEndpoint(MethodName, LanguageServerConstants.DefaultLanguageName)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class TestConfigurableDocumentHandler() : ILspServiceDocumentRequestHandler<TestRequestWithDocument, TestConfigurableResponse>
{
public const string MethodName = nameof(TestConfigurableDocumentHandler);

private bool? _mutatesSolutionState;
private bool? _requiresLSPSolution;
private Task<TestConfigurableResponse>? _response;

public bool MutatesSolutionState => _mutatesSolutionState ?? throw new InvalidOperationException($"{nameof(ConfigureHandler)} has not been called");
public bool RequiresLSPSolution => _requiresLSPSolution ?? throw new InvalidOperationException($"{nameof(ConfigureHandler)} has not been called");

public void ConfigureHandler(bool mutatesSolutionState, bool requiresLspSolution, Task<TestConfigurableResponse> response)
{
if (_mutatesSolutionState is not null || _requiresLSPSolution is not null || _response is not null)
{
throw new InvalidOperationException($"{nameof(ConfigureHandler)} has already been called");
}

_mutatesSolutionState = mutatesSolutionState;
_requiresLSPSolution = requiresLspSolution;
_response = response;
}

public TextDocumentIdentifier GetTextDocumentIdentifier(TestRequestWithDocument request)
{
return request.TextDocumentIdentifier;
}

public Task<TestConfigurableResponse> HandleRequestAsync(TestRequestWithDocument request, RequestContext context, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(_response, $"{nameof(ConfigureHandler)} has not been called");
return _response;
}

public static void ConfigureHandler(TestLspServer server, bool mutatesSolutionState, bool requiresLspSolution, Task<TestConfigurableResponse> response)
{
var handler = (TestConfigurableDocumentHandler)server.GetQueueAccessor()!.Value.GetHandlerProvider().GetMethodHandler(TestConfigurableDocumentHandler.MethodName,
TypeRef.From(typeof(TestRequestWithDocument)), TypeRef.From(typeof(TestConfigurableResponse)), LanguageServerConstants.DefaultLanguageName);
handler.ConfigureHandler(mutatesSolutionState, requiresLspSolution, response);
}
}
Loading