未验证 提交 b65834a3 编写于 作者: M msftbot[bot] 提交者: GitHub

Merge pull request #46660 from davidwengier/RequestExecutionQueue

Request execution queue
......@@ -19,9 +19,13 @@ namespace Microsoft.CodeAnalysis.LanguageServer
internal abstract class AbstractRequestHandlerProvider
{
private readonly ImmutableDictionary<string, Lazy<IRequestHandler, IRequestHandlerMetadata>> _requestHandlers;
private readonly RequestExecutionQueue _queue;
public AbstractRequestHandlerProvider(IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers, string? languageName = null)
=> _requestHandlers = CreateMethodToHandlerMap(requestHandlers.Where(rh => rh.Metadata.LanguageName == languageName));
public AbstractRequestHandlerProvider(IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers, RequestExecutionQueue queue, string? languageName = null)
{
_requestHandlers = CreateMethodToHandlerMap(requestHandlers.Where(rh => rh.Metadata.LanguageName == languageName));
_queue = queue;
}
private static ImmutableDictionary<string, Lazy<IRequestHandler, IRequestHandlerMetadata>> CreateMethodToHandlerMap(IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers)
{
......@@ -40,14 +44,15 @@ public AbstractRequestHandlerProvider(IEnumerable<Lazy<IRequestHandler, IRequest
Contract.ThrowIfNull(request);
Contract.ThrowIfTrue(string.IsNullOrEmpty(methodName), "Invalid method name");
var handler = (IRequestHandler<RequestType, ResponseType>?)_requestHandlers[methodName]?.Value;
var handlerEntry = _requestHandlers[methodName];
Contract.ThrowIfNull(handlerEntry, string.Format("Request handler entry not found for method {0}", methodName));
var mutatesSolutionState = handlerEntry.Metadata.MutatesSolutionState;
var handler = (IRequestHandler<RequestType, ResponseType>?)handlerEntry.Value;
Contract.ThrowIfNull(handler, string.Format("Request handler not found for method {0}", methodName));
var context = CreateContext(clientCapabilities, clientName);
return handler.HandleRequestAsync(request, context, cancellationToken);
return _queue.ExecuteAsync(mutatesSolutionState, handler, request, clientCapabilities, clientName, cancellationToken);
}
private static RequestContext CreateContext(LSP.ClientCapabilities clientCapabilities, string? clientName)
=> new RequestContext(clientCapabilities, clientName);
}
}
......@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Composition;
......@@ -15,9 +17,16 @@ internal class ExportLspMethodAttribute : ExportAttribute, IRequestHandlerMetada
{
public string MethodName { get; }
public string LanguageName { get; }
public string? LanguageName { get; }
/// <summary>
/// Whether or not handling this method results in changes to the current solution state.
/// Mutating requests will block all subsequent requests from starting until after they have
/// completed and mutations have been applied. See <see cref="RequestExecutionQueue"/>.
/// </summary>
public bool MutatesSolutionState { get; }
public ExportLspMethodAttribute(string methodName, string languageName = null) : base(typeof(IRequestHandler))
public ExportLspMethodAttribute(string methodName, string? languageName = null, bool mutatesSolutionState = false) : base(typeof(IRequestHandler))
{
if (string.IsNullOrEmpty(methodName))
{
......@@ -26,6 +35,7 @@ public ExportLspMethodAttribute(string methodName, string languageName = null) :
MethodName = methodName;
LanguageName = languageName;
MutatesSolutionState = mutatesSolutionState;
}
}
}
......@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
internal interface IRequestHandlerMetadata
......@@ -14,6 +16,13 @@ internal interface IRequestHandlerMetadata
/// <summary>
/// Name of the language for LSP method to handle (optional).
/// </summary>
string LanguageName { get; }
string? LanguageName { get; }
/// <summary>
/// Whether or not handling this method results in changes to the current solution state.
/// Mutating requests will block all subsequent requests from starting until after they have
/// completed and mutations have been applied. See <see cref="RequestExecutionQueue"/>.
/// </summary>
bool MutatesSolutionState { get; }
}
}
......@@ -2,11 +2,13 @@
// 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.Threading;
using Microsoft.VisualStudio.LanguageServer.Protocol;
#nullable enable
using System;
using System.Diagnostics;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
/// <summary>
......@@ -14,6 +16,13 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler
/// </summary>
internal readonly struct RequestContext
{
private readonly Action<Solution>? _solutionUpdater;
/// <summary>
/// The solution state that the request should operate on
/// </summary>
public Solution? Solution { get; }
/// <summary>
/// The client capabilities for the request.
/// </summary>
......@@ -25,9 +34,25 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler
public string? ClientName { get; }
public RequestContext(ClientCapabilities clientCapabilities, string? clientName)
: this(null, null, clientCapabilities, clientName)
{
}
internal RequestContext(Solution? solution, Action<Solution>? solutionUpdater, ClientCapabilities clientCapabilities, string? clientName)
{
Solution = solution;
_solutionUpdater = solutionUpdater;
ClientCapabilities = clientCapabilities;
ClientName = clientName;
}
/// <summary>
/// Allows a mutating request to provide a new solution snapshot that all subsequent requests should use.
/// </summary>
public void UpdateSolution(Solution solution)
{
Contract.ThrowIfNull(_solutionUpdater, "Mutating solution not allowed in a non-mutating request handler");
_solutionUpdater?.Invoke(solution);
}
}
}
// 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.
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
internal partial class RequestExecutionQueue
{
private readonly struct QueueItem
{
/// <summary>
/// Processes the queued request, and signals back to the called whether the handler ran to completion.
/// </summary>
/// <remarks>A return value of true does not imply that the request was handled successfully, only that no exception was thrown and the task wasn't cancelled.</remarks>
public readonly Func<RequestContext, CancellationToken, Task<bool>> CallbackAsync;
public readonly bool MutatesSolutionState;
public readonly string? ClientName;
public readonly ClientCapabilities ClientCapabilities;
public readonly CancellationToken CancellationToken;
public QueueItem(bool mutatesSolutionState, ClientCapabilities clientCapabilities, string? clientName, Func<RequestContext, CancellationToken, Task<bool>> callback, CancellationToken cancellationToken)
{
MutatesSolutionState = mutatesSolutionState;
ClientCapabilities = clientCapabilities;
ClientName = clientName;
CallbackAsync = callback;
CancellationToken = cancellationToken;
}
}
}
}
// 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.
#nullable enable
using System;
using System.Composition;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.Threading;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Export(typeof(RequestExecutionQueue)), Shared]
internal partial class RequestExecutionQueue : IDisposable
{
private readonly AsyncQueue<QueueItem> _queue = new AsyncQueue<QueueItem>();
private readonly ILspSolutionProvider _solutionProvider;
private readonly CancellationTokenSource _cancelSource;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RequestExecutionQueue(ILspSolutionProvider solutionProvider)
{
_solutionProvider = solutionProvider;
_cancelSource = new CancellationTokenSource();
// Start the queue processing
_ = ProcessQueueAsync();
}
/// <summary>
/// Queues a request to be handled by the specified handler, with mutating requests blocking subsequent requests
/// from starting until the mutation is complete.
/// </summary>
/// <param name="mutatesSolutionState">Whether or not the specified request needs to mutate the solution.</param>
/// <param name="handler">The handler that will handle the request.</param>
/// <param name="request">The request to handle.</param>
/// <param name="clientCapabilities">The client capabilities.</param>
/// <param name="clientName">The client name.</param>
/// <param name="requestCancellationToken">A cancellation token that will cancel the handing of this request.
/// The request could also be cancelled by the queue shutting down.</param>
/// <returns>A task that can be awaited to observe the results of the handing of this request.</returns>
public Task<TResponseType> ExecuteAsync<TRequestType, TResponseType>(
bool mutatesSolutionState,
IRequestHandler<TRequestType, TResponseType> handler,
TRequestType request,
ClientCapabilities clientCapabilities,
string? clientName,
CancellationToken requestCancellationToken) where TRequestType : class
{
// Create a task completion source that will represent the processing of this request to the caller
var completion = new TaskCompletionSource<TResponseType>();
var item = new QueueItem(mutatesSolutionState, clientCapabilities, clientName,
callback: async (context, cancellationToken) =>
{
// Check if cancellation was requested while this was waiting in the queue
if (cancellationToken.IsCancellationRequested)
{
completion.SetCanceled();
// Tell the queue to ignore any mutations from this request, not that we've given it a chance
// to make any
return false;
}
try
{
var result = await handler.HandleRequestAsync(request, context, cancellationToken).ConfigureAwait(false);
completion.SetResult(result);
// Tell the queue that this was successful so that mutations (if any) can be applied
return true;
}
catch (OperationCanceledException)
{
completion.SetCanceled();
}
catch (Exception exception)
{
// Pass the exception to the task completion source, so the caller of the ExecuteAsync method can observe but
// don't let it escape from this callback, so it doesn't affect the queue processing.
completion.SetException(exception);
}
// Tell the queue to ignore any mutations from this request
return false;
}, requestCancellationToken);
_queue.Enqueue(item);
return completion.Task;
}
private async Task ProcessQueueAsync()
{
// Keep track of solution state modifications made by LSP requests
Solution? lastMutatedSolution = null;
var queueToken = _cancelSource.Token;
while (!queueToken.IsCancellationRequested)
{
var work = await _queue.DequeueAsync().ConfigureAwait(false);
// Create a linked cancellation token to cancel any requests in progress when this shuts down
var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(queueToken, work.CancellationToken).Token;
// The "current" solution can be updated by non-LSP actions, so we need it, but we also need
// to merge in the changes from any mutations that have been applied to open documents
var solution = GetCurrentSolution();
solution = MergeChanges(solution, lastMutatedSolution);
Solution? mutatedSolution = null;
var context = new RequestContext(solution, s => mutatedSolution = s, work.ClientCapabilities, work.ClientName);
if (work.MutatesSolutionState)
{
// Mutating requests block other requests from starting to ensure an up to date snapshot is used.
var ranToCompletion = await work.CallbackAsync(context, cancellationToken).ConfigureAwait(false);
// If the handling of the request failed, the exception will bubble back up to the caller, but we
// still need to react to it here by throwing away solution updates
if (ranToCompletion)
{
lastMutatedSolution = mutatedSolution ?? lastMutatedSolution;
}
}
else
{
// Non mutating request get given the current solution state, but are otherwise fire-and-forget
_ = work.CallbackAsync(context, cancellationToken);
}
}
}
private static Solution MergeChanges(Solution solution, Solution? mutatedSolution)
{
// TODO: Merge in changes to the solution that have been received from didChange LSP methods
// https://github.com/dotnet/roslyn/issues/45427
return mutatedSolution ?? solution;
}
private Solution GetCurrentSolution()
=> _solutionProvider.GetCurrentSolutionForMainWorkspace();
public void Dispose()
{
_cancelSource.Cancel();
_cancelSource.Dispose();
}
}
}
......@@ -28,8 +28,8 @@ internal sealed class LanguageServerProtocol : AbstractRequestHandlerProvider
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public LanguageServerProtocol([ImportMany] IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers)
: base(requestHandlers)
public LanguageServerProtocol([ImportMany] IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers, RequestExecutionQueue queue)
: base(requestHandlers, queue)
{
}
}
......
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
[Shared, ExportLspMethod(MethodName, mutatesSolutionState: true), PartNotDiscoverable]
internal class FailingMutatingRequestHandler : AbstractRequestHandler<TestRequest, TestResponse>
{
public const string MethodName = nameof(FailingMutatingRequestHandler);
private const int Delay = 100;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FailingMutatingRequestHandler(ILspSolutionProvider solutionProvider)
: base(solutionProvider)
{
}
public override async Task<TestResponse> HandleRequestAsync(TestRequest request, RequestContext context, CancellationToken cancellationToken)
{
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
// Mutate the solution
var solution = context.Solution;
solution = solution.WithNewWorkspace(solution.Workspace, solution.WorkspaceVersion + 1);
context.UpdateSolution(solution);
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException();
}
}
}
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
[Shared, ExportLspMethod(MethodName, mutatesSolutionState: false), PartNotDiscoverable]
internal class FailingRequestHandler : AbstractRequestHandler<TestRequest, TestResponse>
{
public const string MethodName = nameof(FailingRequestHandler);
private const int Delay = 100;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FailingRequestHandler(ILspSolutionProvider solutionProvider)
: base(solutionProvider)
{
}
public override async Task<TestResponse> HandleRequestAsync(TestRequest request, RequestContext context, CancellationToken cancellationToken)
{
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException();
}
}
}
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
[Shared, ExportLspMethod(MethodName, mutatesSolutionState: true), PartNotDiscoverable]
internal class MutatingRequestHandler : AbstractRequestHandler<TestRequest, TestResponse>
{
public const string MethodName = nameof(MutatingRequestHandler);
private const int Delay = 100;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public MutatingRequestHandler(ILspSolutionProvider solutionProvider)
: base(solutionProvider)
{
}
public override async Task<TestResponse> HandleRequestAsync(TestRequest request, RequestContext context, CancellationToken cancellationToken)
{
var response = new TestResponse
{
Solution = context.Solution,
RequestOrder = request.RequestOrder,
StartTime = DateTime.UtcNow
};
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
// Mutate the solution
var solution = context.Solution;
solution = solution.WithNewWorkspace(solution.Workspace, solution.WorkspaceVersion + 1);
context.UpdateSolution(solution);
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
response.EndTime = DateTime.UtcNow;
return response;
}
}
}
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
[Shared, ExportLspMethod(MethodName, mutatesSolutionState: false), PartNotDiscoverable]
internal class NonMutatingRequestHandler : AbstractRequestHandler<TestRequest, TestResponse>
{
public const string MethodName = nameof(NonMutatingRequestHandler);
private const int Delay = 100;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public NonMutatingRequestHandler(ILspSolutionProvider solutionProvider)
: base(solutionProvider)
{
}
public override async Task<TestResponse> HandleRequestAsync(TestRequest request, RequestContext context, CancellationToken cancellationToken)
{
var response = new TestResponse();
response.Solution = context.Solution;
response.RequestOrder = request.RequestOrder;
response.StartTime = DateTime.UtcNow;
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
// some busy work
response.ToString();
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
response.EndTime = DateTime.UtcNow;
return response;
}
}
}
// 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.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
public partial class RequestOrderingTests : AbstractLanguageServerProtocolTests
{
protected override TestComposition Composition => base.Composition
.AddParts(typeof(MutatingRequestHandler))
.AddParts(typeof(NonMutatingRequestHandler))
.AddParts(typeof(FailingRequestHandler))
.AddParts(typeof(FailingMutatingRequestHandler));
[Fact]
public async Task MutatingRequestsDontOverlap()
{
var requests = new[] {
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// Every request should have started at or after the one before it
Assert.True(responses[1].StartTime >= responses[0].EndTime);
Assert.True(responses[2].StartTime >= responses[1].EndTime);
}
[Fact]
public async Task NonMutatingRequestsOverlap()
{
var requests = new[] {
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// Every request should have started immediately, without waiting
Assert.True(responses[1].StartTime < responses[0].EndTime);
Assert.True(responses[2].StartTime < responses[1].EndTime);
}
[Fact]
public async Task NonMutatingWaitsForMutating()
{
var requests = new[] {
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// The non mutating tasks should have waited for the first task to finish
Assert.True(responses[1].StartTime >= responses[0].EndTime);
Assert.True(responses[2].StartTime >= responses[0].EndTime);
// The non mutating requests shouldn't have waited for each other
Assert.True(responses[1].StartTime < responses[2].EndTime);
Assert.True(responses[2].StartTime < responses[1].EndTime);
}
[Fact]
public async Task MutatingDoesntWaitForNonMutating()
{
var requests = new[] {
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// All tasks should start without waiting for any to finish
Assert.True(responses[1].StartTime < responses[0].EndTime);
Assert.True(responses[2].StartTime < responses[0].EndTime);
Assert.True(responses[1].StartTime < responses[2].EndTime);
Assert.True(responses[2].StartTime < responses[1].EndTime);
}
[Fact]
public async Task NonMutatingOperatesOnTheRightSolutions()
{
var requests = new[] {
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// first two tasks should have kicked off without waiting
Assert.True(responses[0].StartTime < responses[1].EndTime);
Assert.True(responses[1].StartTime < responses[0].EndTime);
// The mutating task should have kicked off without waiting for those to finish
Assert.True(responses[2].StartTime < responses[1].EndTime);
Assert.True(responses[2].StartTime < responses[0].EndTime);
// The last two tasks should have waited for the mutating task
Assert.True(responses[3].StartTime >= responses[2].EndTime);
Assert.True(responses[4].StartTime >= responses[2].EndTime);
// The last two should have operated on different solutions than the first three
Assert.NotEqual(responses[3].Solution.WorkspaceVersion, responses[0].Solution.WorkspaceVersion);
Assert.NotEqual(responses[3].Solution.WorkspaceVersion, responses[1].Solution.WorkspaceVersion);
Assert.NotEqual(responses[3].Solution.WorkspaceVersion, responses[2].Solution.WorkspaceVersion);
Assert.Equal(responses[3].Solution.WorkspaceVersion, responses[3].Solution.WorkspaceVersion);
}
[Fact]
public async Task ThrowingTaskDoesntBringDownQueue()
{
var requests = new[] {
new TestRequest(FailingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var waitables = StartTestRun(requests);
// first task should fail
await Assert.ThrowsAsync<InvalidOperationException>(async () => await waitables[0]);
// remaining tasks should have executed normally
var responses = await Task.WhenAll(waitables.Skip(1));
Assert.Empty(responses.Where(r => r.StartTime == default));
Assert.All(responses, r => Assert.True(r.EndTime > r.StartTime));
}
[Fact]
public async Task ThrowingMutableTaskDoesntBringDownQueue()
{
var requests = new[] {
new TestRequest(FailingMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var waitables = StartTestRun(requests);
// first task should fail
await Assert.ThrowsAsync<InvalidOperationException>(async () => await waitables[0]);
// remaining tasks should have executed normally
var responses = await Task.WhenAll(waitables.Skip(1));
Assert.Empty(responses.Where(r => r.StartTime == default));
Assert.All(responses, r => Assert.True(r.EndTime > r.StartTime));
}
[Fact]
public async Task ThrowingMutableTaskDoesntMutateTheSolution()
{
var requests = new[] {
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(FailingMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var waitables = StartTestRun(requests);
// second task should have failed
await Assert.ThrowsAsync<InvalidOperationException>(async () => await waitables[1]);
var responses = await Task.WhenAll(waitables.Where(t => !t.IsFaulted));
// First and last tasks use the same solution because the middle request failed
// Note the last task is the _second_ item in responses because it only contains the successful responses
Assert.Equal(responses[0].Solution.WorkspaceVersion, responses[1].Solution.WorkspaceVersion);
}
private async Task<TestResponse[]> TestAsync(TestRequest[] requests)
{
var waitables = StartTestRun(requests);
var responses = await Task.WhenAll(waitables);
// Sanity checks to ensure test handlers aren't doing something wacky, making future checks invalid
Assert.Empty(responses.Where(r => r.StartTime == default));
Assert.All(responses, r => Assert.True(r.EndTime > r.StartTime));
return responses;
}
private List<Task<TestResponse>> StartTestRun(TestRequest[] requests)
{
using var workspace = CreateTestWorkspace("class C { }", out _);
var solution = workspace.CurrentSolution;
var languageServer = GetLanguageServer(solution);
var clientCapabilities = new LSP.ClientCapabilities();
var waitables = new List<Task<TestResponse>>();
var order = 1;
foreach (var request in requests)
{
request.RequestOrder = order++;
waitables.Add(languageServer.ExecuteRequestAsync<TestRequest, TestResponse>(request.MethodName, request, clientCapabilities, null, CancellationToken.None));
}
return waitables;
}
}
}
// 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.
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
internal class TestRequest
{
public string MethodName { get; }
public int RequestOrder { get; set; }
public TestRequest(string methodName)
{
MethodName = methodName;
}
}
}
// 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;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
internal class TestResponse
{
public int RequestOrder { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public Solution Solution { get; set; }
}
}
......@@ -23,8 +23,8 @@ internal sealed class XamlLanguageServerProtocol : AbstractRequestHandlerProvide
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public XamlLanguageServerProtocol([ImportMany] IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers)
: base(requestHandlers, languageName: StringConstants.XamlLanguageName)
public XamlLanguageServerProtocol([ImportMany] IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers, RequestExecutionQueue queue)
: base(requestHandlers, queue, languageName: StringConstants.XamlLanguageName)
{
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册