Fix race with CTS disposing (#11757)

This commit is contained in:
Brennan 2019-10-24 20:54:33 -07:00 committed by GitHub
parent 4481046c45
commit e40b218039
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 61 additions and 23 deletions

View File

@ -253,7 +253,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
if (TransportType == HttpTransportType.WebSockets)
{
// The websocket transport will close the application output automatically when reading is cancelled
// The websocket transport will close the application output automatically when reading is canceled
Cancellation?.Cancel();
}
else
@ -443,6 +443,45 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
}
}
internal async Task<bool> CancelPreviousPoll(HttpContext context)
{
CancellationTokenSource cts;
lock (_stateLock)
{
// Need to sync cts access with DisposeAsync as that will dispose the cts
if (Status == HttpConnectionStatus.Disposed)
{
cts = null;
}
else
{
cts = Cancellation;
Cancellation = null;
}
}
using (cts)
{
// Cancel the previous request
cts?.Cancel();
try
{
// Wait for the previous request to drain
await PreviousPollTask;
}
catch (OperationCanceledException)
{
// Previous poll canceled due to connection closing, close this poll too
context.Response.ContentType = "text/plain";
context.Response.StatusCode = StatusCodes.Status204NoContent;
return false;
}
return true;
}
}
public void MarkInactive()
{
lock (_stateLock)

View File

@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
Log.EstablishedConnection(_logger);
// Allow the reads to be cancelled
// Allow the reads to be canceled
connection.Cancellation = new CancellationTokenSource();
var ws = new WebSocketsServerTransport(options.WebSockets, connection.Application, connection, _loggerFactory);
@ -189,28 +189,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
return;
}
if (!await connection.CancelPreviousPoll(context))
{
// Connection closed. It's already set the response status code.
return;
}
// Create a new Tcs every poll to keep track of the poll finishing, so we can properly wait on previous polls
var currentRequestTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
using (connection.Cancellation)
{
// Cancel the previous request
connection.Cancellation?.Cancel();
try
{
// Wait for the previous request to drain
await connection.PreviousPollTask;
}
catch (OperationCanceledException)
{
// Previous poll canceled due to connection closing, close this poll too
context.Response.ContentType = "text/plain";
context.Response.StatusCode = StatusCodes.Status204NoContent;
return;
}
}
if (!connection.TryActivateLongPollingConnection(
connectionDelegate, context, options.LongPolling.PollTimeout,
currentRequestTcs.Task, _loggerFactory, _logger))

View File

@ -1148,9 +1148,22 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.True(request1.IsCompleted);
request1 = dispatcher.ExecuteAsync(context1, options, app);
var count = 0;
// Wait until the request has started internally
while (connection.TransportTask.IsCompleted && count < 50)
{
count++;
await Task.Delay(15);
}
if (count == 50)
{
Assert.True(false, "Poll took too long to start");
}
var request2 = dispatcher.ExecuteAsync(context2, options, app);
await request1;
// Wait for poll to be canceled
await request1.OrTimeout();
Assert.Equal(StatusCodes.Status204NoContent, context1.Response.StatusCode);
Assert.Equal(HttpConnectionStatus.Active, connection.Status);
@ -1164,7 +1177,6 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
}
[Fact]
[Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/2040", "All")]
public async Task MultipleRequestsToActiveConnectionId409ForLongPolling()
{
using (StartVerifiableLog())