Start each request on fresh ExecutionContext (#14146)

This commit is contained in:
Ben Adams 2020-04-03 01:26:22 +01:00 committed by GitHub
parent 746ac6bc8f
commit f17520c6f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 386 additions and 166 deletions

View File

@ -547,6 +547,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
try try
{ {
// We run the request processing loop in a seperate async method so per connection
// exception handling doesn't complicate the generated asm for the loop.
await ProcessRequests(application); await ProcessRequests(application);
} }
catch (BadHttpRequestException ex) catch (BadHttpRequestException ex)
@ -624,6 +626,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
InitializeBodyControl(messageBody); InitializeBodyControl(messageBody);
// We run user controlled request processing in a seperate async method
// so any changes made to ExecutionContext are undone when it returns and
// each request starts with a fresh ExecutionContext state.
await ProcessRequest(application);
// Even for non-keep-alive requests, try to consume the entire body to avoid RSTs.
if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty)
{
await messageBody.ConsumeAsync();
}
if (HasStartedConsumingRequestBody)
{
await messageBody.StopAsync();
}
}
}
private async ValueTask ProcessRequest<TContext>(IHttpApplication<TContext> application)
{
var context = application.CreateContext(this); var context = application.CreateContext(this);
try try
@ -709,18 +731,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
} }
application.DisposeContext(context, _applicationException); application.DisposeContext(context, _applicationException);
// Even for non-keep-alive requests, try to consume the entire body to avoid RSTs.
if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty)
{
await messageBody.ConsumeAsync();
}
if (HasStartedConsumingRequestBody)
{
await messageBody.StopAsync();
}
}
} }
public void OnStarting(Func<object, Task> callback, object state) public void OnStarting(Func<object, Task> callback, object state)
@ -749,100 +759,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected Task FireOnStarting() protected Task FireOnStarting()
{ {
var onStarting = _onStarting; var onStarting = _onStarting;
if (onStarting?.Count > 0)
if (onStarting == null || onStarting.Count == 0)
{ {
return Task.CompletedTask; return ProcessEvents(this, onStarting);
}
else
{
return FireOnStartingMayAwait(onStarting);
}
}
private Task FireOnStartingMayAwait(Stack<KeyValuePair<Func<object, Task>, object>> onStarting)
{
try
{
while (onStarting.TryPop(out var entry))
{
var task = entry.Key.Invoke(entry.Value);
if (!task.IsCompletedSuccessfully)
{
return FireOnStartingAwaited(task, onStarting);
}
}
}
catch (Exception ex)
{
ReportApplicationError(ex);
} }
return Task.CompletedTask; return Task.CompletedTask;
}
private async Task FireOnStartingAwaited(Task currentTask, Stack<KeyValuePair<Func<object, Task>, object>> onStarting) static async Task ProcessEvents(HttpProtocol protocol, Stack<KeyValuePair<Func<object, Task>, object>> events)
{ {
// Try/Catch is outside the loop as any error that occurs is before the request starts.
// So we want to report it as an ApplicationError to fail the request and not process more events.
try try
{ {
await currentTask; while (events.TryPop(out var entry))
while (onStarting.TryPop(out var entry))
{ {
await entry.Key.Invoke(entry.Value); await entry.Key.Invoke(entry.Value);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
ReportApplicationError(ex); protocol.ReportApplicationError(ex);
}
} }
} }
protected Task FireOnCompleted() protected Task FireOnCompleted()
{ {
var onCompleted = _onCompleted; var onCompleted = _onCompleted;
if (onCompleted?.Count > 0)
if (onCompleted == null || onCompleted.Count == 0)
{ {
return Task.CompletedTask; return ProcessEvents(this, onCompleted);
}
return FireOnCompletedMayAwait(onCompleted);
}
private Task FireOnCompletedMayAwait(Stack<KeyValuePair<Func<object, Task>, object>> onCompleted)
{
while (onCompleted.TryPop(out var entry))
{
try
{
var task = entry.Key.Invoke(entry.Value);
if (!task.IsCompletedSuccessfully)
{
return FireOnCompletedAwaited(task, onCompleted);
}
}
catch (Exception ex)
{
ReportApplicationError(ex);
}
} }
return Task.CompletedTask; return Task.CompletedTask;
}
private async Task FireOnCompletedAwaited(Task currentTask, Stack<KeyValuePair<Func<object, Task>, object>> onCompleted) static async Task ProcessEvents(HttpProtocol protocol, Stack<KeyValuePair<Func<object, Task>, object>> events)
{ {
try // Try/Catch is inside the loop as any error that occurs is after the request has finished.
{ // So we will just log it and keep processing the events, as the completion has already happened.
await currentTask; while (events.TryPop(out var entry))
}
catch (Exception ex)
{
Log.ApplicationError(ConnectionId, TraceIdentifier, ex);
}
while (onCompleted.TryPop(out var entry))
{ {
try try
{ {
@ -850,7 +806,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.ApplicationError(ConnectionId, TraceIdentifier, ex); protocol.Log.ApplicationError(protocol.ConnectionId, protocol.TraceIdentifier, ex);
}
} }
} }
} }

View File

@ -285,6 +285,264 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
} }
} }
[Fact]
public async Task ExecutionContextMutationsOfValueTypeDoNotLeakAcrossRequestsOnSameConnection()
{
var local = new AsyncLocal<int>();
// It's important this method isn't async as that will revert the ExecutionContext
Task ExecuteApplication(HttpContext context)
{
var value = local.Value;
Assert.Equal(0, value);
context.Response.OnStarting(() =>
{
local.Value++;
return Task.CompletedTask;
});
context.Response.OnCompleted(() =>
{
local.Value++;
return Task.CompletedTask;
});
local.Value++;
context.Response.ContentLength = 1;
return context.Response.WriteAsync($"{value}");
}
var testContext = new TestServiceContext(LoggerFactory);
await using var server = new TestServer(ExecuteApplication, testContext);
await TestAsyncLocalValues(testContext, server);
}
[Fact]
public async Task ExecutionContextMutationsOfReferenceTypeDoNotLeakAcrossRequestsOnSameConnection()
{
var local = new AsyncLocal<IntAsClass>();
// It's important this method isn't async as that will revert the ExecutionContext
Task ExecuteApplication(HttpContext context)
{
Assert.Null(local.Value);
local.Value = new IntAsClass();
var value = local.Value.Value;
Assert.Equal(0, value);
context.Response.OnStarting(() =>
{
local.Value.Value++;
return Task.CompletedTask;
});
context.Response.OnCompleted(() =>
{
local.Value.Value++;
return Task.CompletedTask;
});
local.Value.Value++;
context.Response.ContentLength = 1;
return context.Response.WriteAsync($"{value}");
}
var testContext = new TestServiceContext(LoggerFactory);
await using var server = new TestServer(ExecuteApplication, testContext);
await TestAsyncLocalValues(testContext, server);
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
[Fact]
public async Task ExecutionContextMutationsDoNotLeakAcrossAwaits()
{
var local = new AsyncLocal<int>();
// It's important this method isn't async as that will revert the ExecutionContext
Task ExecuteApplication(HttpContext context)
{
var value = local.Value;
Assert.Equal(0, value);
context.Response.OnStarting(async () =>
{
local.Value++;
Assert.Equal(1, local.Value);
});
context.Response.OnCompleted(async () =>
{
local.Value++;
Assert.Equal(1, local.Value);
});
context.Response.ContentLength = 1;
return context.Response.WriteAsync($"{value}");
}
var testContext = new TestServiceContext(LoggerFactory);
await using var server = new TestServer(ExecuteApplication, testContext);
await TestAsyncLocalValues(testContext, server);
}
[Fact]
public async Task ExecutionContextMutationsOfValueTypeFlowIntoButNotOutOfAsyncEvents()
{
var local = new AsyncLocal<int>();
async Task ExecuteApplication(HttpContext context)
{
var value = local.Value;
Assert.Equal(0, value);
context.Response.OnStarting(async () =>
{
local.Value++;
Assert.Equal(2, local.Value);
});
context.Response.OnCompleted(async () =>
{
local.Value++;
Assert.Equal(2, local.Value);
});
local.Value++;
Assert.Equal(1, local.Value);
context.Response.ContentLength = 1;
await context.Response.WriteAsync($"{value}");
local.Value++;
Assert.Equal(2, local.Value);
}
var testContext = new TestServiceContext(LoggerFactory);
await using var server = new TestServer(ExecuteApplication, testContext);
await TestAsyncLocalValues(testContext, server);
}
[Fact]
public async Task ExecutionContextMutationsOfReferenceTypeFlowThroughAsyncEvents()
{
var local = new AsyncLocal<IntAsClass>();
async Task ExecuteApplication(HttpContext context)
{
Assert.Null(local.Value);
local.Value = new IntAsClass();
var value = local.Value.Value;
Assert.Equal(0, value); // Start
context.Response.OnStarting(async () =>
{
local.Value.Value++;
Assert.Equal(2, local.Value.Value); // Second
});
context.Response.OnCompleted(async () =>
{
local.Value.Value++;
Assert.Equal(4, local.Value.Value); // Fourth
});
local.Value.Value++;
Assert.Equal(1, local.Value.Value); // First
context.Response.ContentLength = 1;
await context.Response.WriteAsync($"{value}");
local.Value.Value++;
Assert.Equal(3, local.Value.Value); // Third
}
var testContext = new TestServiceContext(LoggerFactory);
await using var server = new TestServer(ExecuteApplication, testContext);
await TestAsyncLocalValues(testContext, server);
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
[Fact]
public async Task ExecutionContextMutationsOfValueTypeFlowIntoButNotOutOfNonAsyncEvents()
{
var local = new AsyncLocal<int>();
async Task ExecuteApplication(HttpContext context)
{
var value = local.Value;
Assert.Equal(0, value);
context.Response.OnStarting(() =>
{
local.Value++;
Assert.Equal(2, local.Value);
return Task.CompletedTask;
});
context.Response.OnCompleted(() =>
{
local.Value++;
Assert.Equal(2, local.Value);
return Task.CompletedTask;
});
local.Value++;
Assert.Equal(1, local.Value);
context.Response.ContentLength = 1;
await context.Response.WriteAsync($"{value}");
local.Value++;
Assert.Equal(2, local.Value);
}
var testContext = new TestServiceContext(LoggerFactory);
await using var server = new TestServer(ExecuteApplication, testContext);
await TestAsyncLocalValues(testContext, server);
}
private static async Task TestAsyncLocalValues(TestServiceContext testContext, TestServer server)
{
using var connection = server.CreateConnection();
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 1",
"",
"0");
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 1",
"",
"0");
}
[Fact] [Fact]
public async Task AppCanSetTraceIdentifier() public async Task AppCanSetTraceIdentifier()
{ {
@ -1803,5 +2061,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
} }
public static TheoryData<string, string> HostHeaderData => HttpParsingData.HostHeaderData; public static TheoryData<string, string> HostHeaderData => HttpParsingData.HostHeaderData;
private class IntAsClass
{
public int Value;
}
} }
} }