-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
Version Used: 4.12, but seems to be present in main too
Steps to Reproduce:
- Have a solution which takes a long time to load
- Pass a cancellation token to
MSBuildWorkspace.OpenSolutionAsync - Try to cancel the load
The issue is that RpcClient.InvokeCoreAsync does not respect the passed cancellation token when doing the final await.
Expected Behavior:
The operation is cancelled.
Actual Behavior:
The operation is not cancelled.
Machine-generated report contains more information
# Bug Report: RpcClient.InvokeCoreAsync doesn't honor CancellationToken when waiting for responseSummary
The RpcClient.InvokeCoreAsync method in Microsoft.CodeAnalysis.MSBuild doesn't honor the provided CancellationToken when waiting for the server response, causing operations to hang indefinitely even when cancelled.
Location
- File:
src/Workspaces/Core/MSBuild/Rpc/RpcClient.cs - Method:
private async Task<object?> InvokeCoreAsync(...) - Line: ~150 (final
return awaitstatement)
Problem Description
The InvokeCoreAsync method correctly handles cancellation during the sending phase of an RPC request (lines ~139-147), but completely ignores the cancellationToken when waiting for the response (line ~150):
return await requestCompletionSource.Task.ConfigureAwait(continueOnCapturedContext: false);This causes the following issues:
- If the server is slow or unresponsive, the operation hangs indefinitely
- When a caller cancels the operation (e.g., via timeout), the cancellation is ignored
- The
requestCompletionSource.Taskwill never complete unless the server responds or disconnects
Reproduction Steps
- Call
MSBuildWorkspace.OpenSolutionAsync()with aCancellationToken - Have the MSBuild server process be slow or hang during solution loading
- Cancel the
CancellationToken(e.g., viaCancellationTokenSource.CancelAfter()) - Observe that the operation does not cancel and continues waiting
Expected Behavior
When the cancellationToken is cancelled, the InvokeCoreAsync method should:
- Cancel the pending request
- Remove the request from
_outstandingRequests - Throw
OperationCanceledException
Actual Behavior
The operation continues waiting indefinitely for the server response, ignoring the cancellation request.
Proposed Fix
Option 1: Use Task.WaitAsync (Recommended for .NET 6+)
return await requestCompletionSource.Task.WaitAsync(cancellationToken)
.ConfigureAwait(continueOnCapturedContext: false);Option 2: Register cancellation callback
using (cancellationToken.Register(() =>
{
_outstandingRequests.TryRemove(requestId, out _);
requestCompletionSource.TrySetCanceled(cancellationToken);
}))
{
return await requestCompletionSource.Task.ConfigureAwait(continueOnCapturedContext: false);
}Impact
This bug affects any code that:
- Uses
MSBuildWorkspace.OpenSolutionAsync()orOpenProjectAsync()with timeouts - Needs to cancel long-running MSBuild operations
- Relies on cancellation tokens for resource cleanup or timeout enforcement
Workaround
Callers can work around this by using Task.WhenAny to enforce their own timeout:
var openTask = workspace.OpenSolutionAsync(solutionPath, cancellationToken);
var timeoutTask = Task.Delay(timeout, CancellationToken.None);
if (await Task.WhenAny(openTask, timeoutTask) == timeoutTask)
{
workspace.Dispose();
throw new TimeoutException("Operation timed out");
}
await openTask; // Propagate any exceptionsAdditional Context
- Found while debugging integration tests that use MSBuildWorkspace with 60-second timeouts
- The cancellation token shows
IsCancellationRequested = truein the debugger, but the await continues indefinitely - This is a correctness issue with async/await patterns where cancellation tokens should be honored throughout the entire async operation
Related Code Paths
All public methods that call InvokeCoreAsync are affected:
InvokeAsync(int targetObject, ...)InvokeNullableAsync<T>(...)InvokeAsync<T>(...)
All of these ultimately call InvokeCoreAsync which has the bug.