Better context pooling (#12385)

- This change goes from pooling just the HttpContext to pooling the entire TContext. In the past this was a huge struct that got copied around and now it can be a class. Servers can provide the storage for the TContext via a new `IHostContextContainer<TContext>` interface. 
- Removed IDefaultHttpContextContainer since it's been superseded by IHostContextContainer
- Move DefaultHttpContextFactory to Hosting to take advantage of internal methods
- Also handle a null FeatureCollection and null HttpContext and throw a better exception
This commit is contained in:
Ben Adams 2019-07-20 21:07:20 +01:00 committed by David Fowler
parent 4ac6a4ad35
commit 65ca72c420
18 changed files with 250 additions and 117 deletions

View File

@ -85,6 +85,15 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets
public static void UseStaticWebAssets(Microsoft.AspNetCore.Hosting.IWebHostEnvironment environment, Microsoft.Extensions.Configuration.IConfiguration configuration) { }
}
}
namespace Microsoft.AspNetCore.Http
{
public partial class DefaultHttpContextFactory : Microsoft.AspNetCore.Http.IHttpContextFactory
{
public DefaultHttpContextFactory(System.IServiceProvider serviceProvider) { }
public Microsoft.AspNetCore.Http.HttpContext Create(Microsoft.AspNetCore.Http.Features.IFeatureCollection featureCollection) { throw null; }
public void Dispose(Microsoft.AspNetCore.Http.HttpContext httpContext) { }
}
}
namespace Microsoft.Extensions.Hosting
{
public static partial class GenericHostWebHostBuilderExtensions

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -26,12 +28,30 @@ namespace Microsoft.AspNetCore.Http
public HttpContext Create(IFeatureCollection featureCollection)
{
if (featureCollection == null)
if (featureCollection is null)
{
throw new ArgumentNullException(nameof(featureCollection));
}
var httpContext = CreateHttpContext(featureCollection);
var httpContext = new DefaultHttpContext(featureCollection);
Initialize(httpContext);
return httpContext;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Initialize(DefaultHttpContext httpContext, IFeatureCollection featureCollection)
{
Debug.Assert(featureCollection != null);
Debug.Assert(httpContext != null);
httpContext.Initialize(featureCollection);
Initialize(httpContext);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private DefaultHttpContext Initialize(DefaultHttpContext httpContext)
{
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = httpContext;
@ -43,16 +63,6 @@ namespace Microsoft.AspNetCore.Http
return httpContext;
}
private static DefaultHttpContext CreateHttpContext(IFeatureCollection featureCollection)
{
if (featureCollection is IDefaultHttpContextContainer container)
{
return container.HttpContext;
}
return new DefaultHttpContext(featureCollection);
}
public void Dispose(HttpContext httpContext)
{
if (_httpContextAccessor != null)
@ -60,5 +70,15 @@ namespace Microsoft.AspNetCore.Http
_httpContextAccessor.HttpContext = null;
}
}
internal void Dispose(DefaultHttpContext httpContext)
{
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = null;
}
httpContext.Uninitialize();
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Abstractions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
@ -15,6 +16,7 @@ namespace Microsoft.AspNetCore.Hosting
{
private readonly RequestDelegate _application;
private readonly IHttpContextFactory _httpContextFactory;
private readonly DefaultHttpContextFactory _defaultHttpContextFactory;
private HostingApplicationDiagnostics _diagnostics;
public HostingApplication(
@ -25,19 +27,58 @@ namespace Microsoft.AspNetCore.Hosting
{
_application = application;
_diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource);
_httpContextFactory = httpContextFactory;
if (httpContextFactory is DefaultHttpContextFactory factory)
{
_defaultHttpContextFactory = factory;
}
else
{
_httpContextFactory = httpContextFactory;
}
}
// Set up the request
public Context CreateContext(IFeatureCollection contextFeatures)
{
var context = new Context();
var httpContext = _httpContextFactory.Create(contextFeatures);
Context hostContext;
if (contextFeatures is IHostContextContainer<Context> container)
{
hostContext = container.HostContext;
if (hostContext is null)
{
hostContext = new Context();
container.HostContext = hostContext;
}
}
else
{
// Server doesn't support pooling, so create a new Context
hostContext = new Context();
}
_diagnostics.BeginRequest(httpContext, ref context);
HttpContext httpContext;
if (_defaultHttpContextFactory != null)
{
var defaultHttpContext = (DefaultHttpContext)hostContext.HttpContext;
if (defaultHttpContext is null)
{
httpContext = _defaultHttpContextFactory.Create(contextFeatures);
hostContext.HttpContext = httpContext;
}
else
{
_defaultHttpContextFactory.Initialize(defaultHttpContext, contextFeatures);
httpContext = defaultHttpContext;
}
}
else
{
httpContext = _httpContextFactory.Create(contextFeatures);
hostContext.HttpContext = httpContext;
}
context.HttpContext = httpContext;
return context;
_diagnostics.BeginRequest(httpContext, hostContext);
return hostContext;
}
// Execute the request
@ -51,18 +92,44 @@ namespace Microsoft.AspNetCore.Hosting
{
var httpContext = context.HttpContext;
_diagnostics.RequestEnd(httpContext, exception, context);
_httpContextFactory.Dispose(httpContext);
if (_defaultHttpContextFactory != null)
{
_defaultHttpContextFactory.Dispose((DefaultHttpContext)httpContext);
}
else
{
_httpContextFactory.Dispose(httpContext);
}
_diagnostics.ContextDisposed(context);
// Reset the context as it may be pooled
context.Reset();
}
internal struct Context
internal class Context
{
public HttpContext HttpContext { get; set; }
public IDisposable Scope { get; set; }
public long StartTimestamp { get; set; }
public bool EventLogEnabled { get; set; }
public Activity Activity { get; set; }
public long StartTimestamp { get; set; }
internal bool HasDiagnosticListener { get; set; }
public bool EventLogEnabled { get; set; }
public void Reset()
{
// Not resetting HttpContext here as we pool it on the Context
Scope = null;
Activity = null;
StartTimestamp = 0;
HasDiagnosticListener = false;
EventLogEnabled = false;
}
}
}
}

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Hosting
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void BeginRequest(HttpContext httpContext, ref HostingApplication.Context context)
public void BeginRequest(HttpContext httpContext, HostingApplication.Context context)
{
long startTimestamp = 0;

View File

@ -1,11 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Http

View File

@ -883,23 +883,21 @@ namespace Microsoft.AspNetCore.Hosting
public async Task WebHost_CreatesDefaultRequestIdentifierFeature_IfNotPresent()
{
// Arrange
HttpContext httpContext = null;
var requestDelegate = new RequestDelegate(innerHttpContext =>
{
httpContext = innerHttpContext;
return Task.FromResult(0);
});
using (var host = CreateHost(requestDelegate))
var requestDelegate = new RequestDelegate(httpContext =>
{
// Act
await host.StartAsync();
// Assert
Assert.NotNull(httpContext);
var featuresTraceIdentifier = httpContext.Features.Get<IHttpRequestIdentifierFeature>().TraceIdentifier;
Assert.False(string.IsNullOrWhiteSpace(httpContext.TraceIdentifier));
Assert.Same(httpContext.TraceIdentifier, featuresTraceIdentifier);
return Task.CompletedTask;
});
using (var host = CreateHost(requestDelegate))
{
// Act
await host.StartAsync();
}
}
@ -907,13 +905,15 @@ namespace Microsoft.AspNetCore.Hosting
public async Task WebHost_DoesNot_CreateDefaultRequestIdentifierFeature_IfPresent()
{
// Arrange
HttpContext httpContext = null;
var requestDelegate = new RequestDelegate(innerHttpContext =>
{
httpContext = innerHttpContext;
return Task.FromResult(0);
});
var requestIdentifierFeature = new StubHttpRequestIdentifierFeature();
var requestDelegate = new RequestDelegate(httpContext =>
{
// Assert
Assert.NotNull(httpContext);
Assert.Same(requestIdentifierFeature, httpContext.Features.Get<IHttpRequestIdentifierFeature>());
return Task.CompletedTask;
});
using (var host = CreateHost(requestDelegate))
{
@ -926,10 +926,6 @@ namespace Microsoft.AspNetCore.Hosting
};
// Act
await host.StartAsync();
// Assert
Assert.NotNull(httpContext);
Assert.Same(requestIdentifierFeature, httpContext.Features.Get<IHttpRequestIdentifierFeature>());
}
}
@ -949,6 +945,36 @@ namespace Microsoft.AspNetCore.Hosting
}
}
[Fact]
public async Task WebHost_HttpContextUseAfterRequestEnd_Fails()
{
// Arrange
HttpContext capturedContext = null;
HttpRequest capturedRequest = null;
var requestDelegate = new RequestDelegate(httpContext =>
{
capturedContext = httpContext;
capturedRequest = httpContext.Request;
return Task.CompletedTask;
});
using (var host = CreateHost(requestDelegate))
{
// Act
await host.StartAsync();
// Assert
Assert.NotNull(capturedContext);
Assert.NotNull(capturedRequest);
Assert.Throws<ObjectDisposedException>(() => capturedContext.TraceIdentifier);
Assert.Throws<ObjectDisposedException>(() => capturedContext.Features.Get<IHttpRequestIdentifierFeature>());
Assert.Throws<ObjectDisposedException>(() => capturedRequest.Scheme);
}
}
public class CountStartup
{
public static int ConfigureServicesCount;

View File

@ -27,6 +27,13 @@ namespace Microsoft.AspNetCore.Hosting.Server
public bool IsEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
}
namespace Microsoft.AspNetCore.Hosting.Server.Abstractions
{
public partial interface IHostContextContainer<TContext>
{
TContext HostContext { get; set; }
}
}
namespace Microsoft.AspNetCore.Hosting.Server.Features
{
public partial interface IServerAddressesFeature

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Hosting.Server.Abstractions
{
/// <summary>
/// When implemented by a Server allows an <see cref="IHttpApplication{TContext}"/> to pool and reuse
/// its <typeparamref name="TContext"/> between requests.
/// </summary>
/// <typeparam name="TContext">The <see cref="IHttpApplication{TContext}"/> Host context</typeparam>
public interface IHostContextContainer<TContext>
{
TContext HostContext { get; set; }
}
}

View File

@ -2,11 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Microsoft.AspNetCore.TestHost.Tests
@ -19,7 +17,9 @@ namespace Microsoft.AspNetCore.TestHost.Tests
[InlineData("http://localhost:81/connect", "localhost:81")]
public async Task ConnectAsync_ShouldSetRequestProperties(string requestUri, string expectedHost)
{
HttpRequest capturedRequest = null;
string capturedScheme = null;
string capturedHost = null;
string capturedPath = null;
using (var testServer = new TestServer(new WebHostBuilder()
.Configure(app =>
@ -28,7 +28,9 @@ namespace Microsoft.AspNetCore.TestHost.Tests
{
if (ctx.Request.Path.StartsWithSegments("/connect"))
{
capturedRequest = ctx.Request;
capturedScheme = ctx.Request.Scheme;
capturedHost = ctx.Request.Host.Value;
capturedPath = ctx.Request.Path;
}
return Task.FromResult(0);
});
@ -40,7 +42,7 @@ namespace Microsoft.AspNetCore.TestHost.Tests
{
await client.ConnectAsync(
uri: new Uri(requestUri),
cancellationToken: default(CancellationToken));
cancellationToken: default);
}
catch
{
@ -48,9 +50,9 @@ namespace Microsoft.AspNetCore.TestHost.Tests
}
}
Assert.Equal("http", capturedRequest.Scheme);
Assert.Equal(expectedHost, capturedRequest.Host.Value);
Assert.Equal("/connect", capturedRequest.Path);
Assert.Equal("http", capturedScheme);
Assert.Equal(expectedHost, capturedHost);
Assert.Equal("/connect", capturedPath);
}
}
}

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Http.Features
public FeatureReferences(IFeatureCollection collection)
{
Collection = collection;
Cache = default(TCache);
Cache = default;
Revision = collection.Revision;
}
@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Http.Features
Func<TState, TFeature> factory) where TFeature : class
{
var flush = false;
var revision = Collection.Revision;
var revision = Collection?.Revision ?? ContextDisposed();
if (Revision != revision)
{
// Clear cached value to force call to UpdateCached
@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Http.Features
if (flush)
{
// Collection detected as changed, clear cache
Cache = default(TCache);
Cache = default;
}
cached = Collection.Get<TFeature>();
@ -108,5 +108,16 @@ namespace Microsoft.AspNetCore.Http.Features
public TFeature Fetch<TFeature>(ref TFeature cached, Func<IFeatureCollection, TFeature> factory)
where TFeature : class => Fetch(ref cached, Collection, factory);
private static int ContextDisposed()
{
ThrowContextDisposed();
return 0;
}
private static void ThrowContextDisposed()
{
throw new ObjectDisposedException(nameof(Collection), nameof(IFeatureCollection) + " has been disposed.");
}
}
}

View File

@ -54,12 +54,6 @@ namespace Microsoft.AspNetCore.Http
public void Initialize(Microsoft.AspNetCore.Http.Features.IFeatureCollection features) { }
public void Uninitialize() { }
}
public partial class DefaultHttpContextFactory : Microsoft.AspNetCore.Http.IHttpContextFactory
{
public DefaultHttpContextFactory(System.IServiceProvider serviceProvider) { }
public Microsoft.AspNetCore.Http.HttpContext Create(Microsoft.AspNetCore.Http.Features.IFeatureCollection featureCollection) { throw null; }
public void Dispose(Microsoft.AspNetCore.Http.HttpContext httpContext) { }
}
public partial class FormCollection : Microsoft.AspNetCore.Http.IFormCollection, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, Microsoft.Extensions.Primitives.StringValues>>, System.Collections.IEnumerable
{
public static readonly Microsoft.AspNetCore.Http.FormCollection Empty;
@ -164,10 +158,6 @@ namespace Microsoft.AspNetCore.Http
public static void EnableBuffering(this Microsoft.AspNetCore.Http.HttpRequest request, int bufferThreshold, long bufferLimit) { }
public static void EnableBuffering(this Microsoft.AspNetCore.Http.HttpRequest request, long bufferLimit) { }
}
public partial interface IDefaultHttpContextContainer
{
Microsoft.AspNetCore.Http.DefaultHttpContext HttpContext { get; }
}
public partial class MiddlewareFactory : Microsoft.AspNetCore.Http.IMiddlewareFactory
{
public MiddlewareFactory(System.IServiceProvider serviceProvider) { }

View File

@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Http
private IHttpRequestIdentifierFeature RequestIdentifierFeature =>
_features.Fetch(ref _features.Cache.RequestIdentifier, _newHttpRequestIdentifierFeature);
public override IFeatureCollection Features => _features.Collection;
public override IFeatureCollection Features => _features.Collection ?? ContextDisposed();
public override HttpRequest Request => _request;
@ -169,6 +169,17 @@ namespace Microsoft.AspNetCore.Http
LifetimeFeature.Abort();
}
private static IFeatureCollection ContextDisposed()
{
ThrowContextDisposed();
return null;
}
private static void ThrowContextDisposed()
{
throw new ObjectDisposedException(nameof(HttpContext), $"Request has finished and {nameof(HttpContext)} disposed.");
}
struct FeatureInterfaces
{
public IItemsFeature Items;

View File

@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Http
throw new ArgumentNullException(nameof(featureCollection));
}
var httpContext = CreateHttpContext(featureCollection);
var httpContext = new DefaultHttpContext(featureCollection);
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = httpContext;
@ -66,16 +66,6 @@ namespace Microsoft.AspNetCore.Http
return httpContext;
}
private static DefaultHttpContext CreateHttpContext(IFeatureCollection featureCollection)
{
if (featureCollection is IDefaultHttpContextContainer container)
{
return container.HttpContext;
}
return new DefaultHttpContext(featureCollection);
}
public void Dispose(HttpContext httpContext)
{
if (_httpContextAccessor != null)

View File

@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.AspNetCore.Http
{
public interface IDefaultHttpContextContainer
{
DefaultHttpContext HttpContext { get; }
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting.Server.Abstractions;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
internal sealed class Http1Connection<TContext> : Http1Connection, IHostContextContainer<TContext>
{
public Http1Connection(HttpConnectionContext context) : base(context) { }
TContext IHostContextContainer<TContext>.HostContext { get; set; }
}
}

View File

@ -25,7 +25,7 @@ using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
internal abstract partial class HttpProtocol : IDefaultHttpContextContainer, IHttpResponseControl
internal abstract partial class HttpProtocol : IHttpResponseControl
{
private static readonly byte[] _bytesConnectionClose = Encoding.ASCII.GetBytes("\r\nConnection: close");
private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive");
@ -64,7 +64,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private long _responseBytesWritten;
private readonly HttpConnectionContext _context;
private DefaultHttpContext _httpContext;
private RouteValueDictionary _routeValues;
private Endpoint _endpoint;
@ -296,23 +295,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders();
DefaultHttpContext IDefaultHttpContextContainer.HttpContext
{
get
{
if (_httpContext is null)
{
_httpContext = new DefaultHttpContext(this);
}
else
{
_httpContext.Initialize(this);
}
return _httpContext;
}
}
public void InitializeBodyControl(MessageBody messageBody)
{
if (_bodyControl == null)
@ -409,8 +391,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_responseBytesWritten = 0;
_httpContext?.Uninitialize();
OnReset();
}

View File

@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Abstractions;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
internal sealed class Http2Stream<TContext> : Http2Stream
internal sealed class Http2Stream<TContext> : Http2Stream, IHostContextContainer<TContext>
{
private readonly IHttpApplication<TContext> _application;
@ -19,5 +21,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// REVIEW: Should we store this in a field for easy debugging?
_ = ProcessRequestsAsync(_application);
}
// Pooled Host context
TContext IHostContextContainer<TContext>.HostContext { get; set; }
}
}

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
case HttpProtocols.Http1:
// _http1Connection must be initialized before adding the connection to the connection manager
requestProcessor = _http1Connection = new Http1Connection(_context);
requestProcessor = _http1Connection = new Http1Connection<TContext>(_context);
_protocolSelectionState = ProtocolSelectionState.Selected;
break;
case HttpProtocols.Http2: