Split lock into WriteLock and StateLock to prevent deadlocks and unneeded lock contention (#2081)

This commit is contained in:
BrennanConroy 2018-04-18 17:44:05 -07:00 committed by GitHub
parent 903fe1e902
commit e9937ffb66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 20 deletions

View File

@ -80,7 +80,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
public HttpTransportType TransportType { get; set; } public HttpTransportType TransportType { get; set; }
public SemaphoreSlim Lock { get; } = new SemaphoreSlim(1, 1); public SemaphoreSlim WriteLock { get; } = new SemaphoreSlim(1, 1);
public SemaphoreSlim StateLock { get; } = new SemaphoreSlim(1, 1);
// Used for testing only // Used for testing only
internal Task DisposeAndRemoveTask { get; set; } internal Task DisposeAndRemoveTask { get; set; }
@ -179,10 +180,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
{ {
var disposeTask = Task.CompletedTask; var disposeTask = Task.CompletedTask;
await StateLock.WaitAsync();
try try
{ {
await Lock.WaitAsync();
if (Status == HttpConnectionStatus.Disposed) if (Status == HttpConnectionStatus.Disposed)
{ {
disposeTask = _disposeTcs.Task; disposeTask = _disposeTcs.Task;
@ -201,7 +201,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
} }
finally finally
{ {
Lock.Release(); StateLock.Release();
} }
await disposeTask; await disposeTask;

View File

@ -188,10 +188,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
return; return;
} }
await connection.StateLock.WaitAsync();
try try
{ {
await connection.Lock.WaitAsync();
if (connection.Status == HttpConnectionStatus.Disposed) if (connection.Status == HttpConnectionStatus.Disposed)
{ {
Log.ConnectionDisposed(_logger, connection.ConnectionId); Log.ConnectionDisposed(_logger, connection.ConnectionId);
@ -263,7 +262,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
} }
finally finally
{ {
connection.Lock.Release(); connection.StateLock.Release();
} }
var resultTask = await Task.WhenAny(connection.ApplicationTask, connection.TransportTask); var resultTask = await Task.WhenAny(connection.ApplicationTask, connection.TransportTask);
@ -299,10 +298,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
if (pollAgain) if (pollAgain)
{ {
// Otherwise, we update the state to inactive again and wait for the next poll // Otherwise, we update the state to inactive again and wait for the next poll
await connection.StateLock.WaitAsync();
try try
{ {
await connection.Lock.WaitAsync();
if (connection.Status == HttpConnectionStatus.Active) if (connection.Status == HttpConnectionStatus.Active)
{ {
// Mark the connection as inactive // Mark the connection as inactive
@ -318,7 +316,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
} }
finally finally
{ {
connection.Lock.Release(); connection.StateLock.Release();
} }
} }
} }
@ -329,10 +327,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
HttpContext context, HttpContext context,
HttpConnectionContext connection) HttpConnectionContext connection)
{ {
await connection.StateLock.WaitAsync();
try try
{ {
await connection.Lock.WaitAsync();
if (connection.Status == HttpConnectionStatus.Disposed) if (connection.Status == HttpConnectionStatus.Disposed)
{ {
Log.ConnectionDisposed(_logger, connection.ConnectionId); Log.ConnectionDisposed(_logger, connection.ConnectionId);
@ -363,7 +360,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
} }
finally finally
{ {
connection.Lock.Release(); connection.StateLock.Release();
} }
// Wait for any of them to end // Wait for any of them to end
@ -468,9 +465,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
const int bufferSize = 4096; const int bufferSize = 4096;
// REVIEW: Consider spliting the connection lock into a read lock and a write lock await connection.WriteLock.WaitAsync();
// Need to think about HttpConnectionContext.DisposeAsync and whether one or both locks would be needed
await connection.Lock.WaitAsync();
try try
{ {
@ -493,7 +488,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
} }
finally finally
{ {
connection.Lock.Release(); connection.WriteLock.Release();
} }
} }

View File

@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
DateTimeOffset lastSeenUtc; DateTimeOffset lastSeenUtc;
var connection = c.Value.Connection; var connection = c.Value.Connection;
await connection.Lock.WaitAsync(); await connection.StateLock.WaitAsync();
try try
{ {
@ -153,7 +153,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
} }
finally finally
{ {
connection.Lock.Release(); connection.StateLock.Release();
} }
// Once the decision has been made to dispose we don't check the status again // Once the decision has been made to dispose we don't check the status again

View File

@ -1390,7 +1390,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app); connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app);
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).AsTask().OrTimeout(); await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).AsTask().OrTimeout();
await connectionHandlerTask.OrTimeout(); await connectionHandlerTask.OrTimeout();
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
Assert.Equal("Hello, World", GetContentAsString(context.Response.Body)); Assert.Equal("Hello, World", GetContentAsString(context.Response.Body));
} }
@ -1797,6 +1797,58 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
} }
} }
[Fact]
public async Task LongPollingCanPollIfWritePipeHasBackpressure()
{
using (StartVerifableLog(out var loggerFactory, LogLevel.Debug))
{
var manager = CreateConnectionManager(loggerFactory);
var pipeOptions = new PipeOptions(pauseWriterThreshold: 13, resumeWriterThreshold: 10);
var connection = manager.CreateConnection(pipeOptions, pipeOptions);
connection.TransportType = HttpTransportType.LongPolling;
var dispatcher = new HttpConnectionDispatcher(manager, loggerFactory);
var services = new ServiceCollection();
services.AddSingleton<TestConnectionHandler>();
var builder = new ConnectionBuilder(services.BuildServiceProvider());
builder.UseConnectionHandler<TestConnectionHandler>();
var app = builder.Build();
var options = new HttpConnectionDispatcherOptions();
using (var responseBody = new MemoryStream())
using (var requestBody = new MemoryStream())
{
var context = new DefaultHttpContext();
context.Request.Body = requestBody;
context.Response.Body = responseBody;
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
var qs = new QueryCollection(values);
context.Request.Query = qs;
var buffer = Encoding.UTF8.GetBytes("Hello, world");
requestBody.Write(buffer, 0, buffer.Length);
requestBody.Seek(0, SeekOrigin.Begin);
// Write some data to the pipe to fill it up and make the next write wait
await connection.ApplicationStream.WriteAsync(buffer, 0, buffer.Length).OrTimeout();
// This will block until the pipe is unblocked
var sendTask = dispatcher.ExecuteAsync(context, options, app).OrTimeout();
Assert.False(sendTask.IsCompleted);
var pollContext = MakeRequest("/foo", connection);
// This should unblock the send that is waiting because of backpressure
// Testing deadlock regression where pipe backpressure would hold the same lock that poll would use
await dispatcher.ExecuteAsync(pollContext, options, app).OrTimeout();
await sendTask.OrTimeout();
}
}
}
private class RejectHandler : TestAuthenticationHandler private class RejectHandler : TestAuthenticationHandler
{ {
protected override bool ShouldAccept => false; protected override bool ShouldAccept => false;