#121 Enable kernel mode response caching.

This commit is contained in:
Chris R 2015-06-12 12:12:22 -07:00
parent 3c044fb92e
commit 20f2219886
10 changed files with 1410 additions and 5 deletions

View File

@ -27,6 +27,7 @@ using System.Threading.Tasks;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Features.Authentication;
using Microsoft.Net.Http.Headers;
using Microsoft.Net.Http.Server;
using Microsoft.Net.WebSockets;
@ -46,8 +47,11 @@ namespace Microsoft.AspNet.Server.WebListener
IHttpUpgradeFeature,
IRequestIdentifierFeature
{
private static Action<object> OnStartDelegate = OnStart;
private RequestContext _requestContext;
private FeatureCollection _features;
private bool _enableResponseCaching;
private Stream _requestBody;
private IDictionary<string, string[]> _requestHeaders;
@ -69,11 +73,13 @@ namespace Microsoft.AspNet.Server.WebListener
private Stream _responseStream;
private IDictionary<string, string[]> _responseHeaders;
internal FeatureContext(RequestContext requestContext)
internal FeatureContext(RequestContext requestContext, bool enableResponseCaching)
{
_requestContext = requestContext;
_features = new FeatureCollection();
_authHandler = new AuthenticationHandler(requestContext);
_enableResponseCaching = enableResponseCaching;
requestContext.Response.OnSendingHeaders(OnStartDelegate, this);
PopulateFeatures();
}
@ -471,5 +477,69 @@ namespace Microsoft.AspNet.Server.WebListener
return _requestContext.TraceIdentifier;
}
}
private static void OnStart(object obj)
{
var featureContext = (FeatureContext)obj;
ConsiderEnablingResponseCache(featureContext);
}
private static void ConsiderEnablingResponseCache(FeatureContext featureContext)
{
if (featureContext._enableResponseCaching)
{
// We don't have to worry too much about what Http.Sys supports, caching is a best-effort feature.
// If there's something about the request or response that prevents it from caching then the response
// will complete normally without caching.
featureContext._requestContext.Response.CacheTtl = GetCacheTtl(featureContext._requestContext);
}
}
private static TimeSpan? GetCacheTtl(RequestContext requestContext)
{
var response = requestContext.Response;
// Only consider kernel-mode caching if the Cache-Control response header is present.
var cacheControlHeader = response.Headers[HeaderNames.CacheControl];
if (string.IsNullOrEmpty(cacheControlHeader))
{
return null;
}
// Before we check the header value, check for the existence of other headers which would
// make us *not* want to cache the response.
if (response.Headers.ContainsKey(HeaderNames.SetCookie)
|| response.Headers.ContainsKey(HeaderNames.Vary)
|| response.Headers.ContainsKey(HeaderNames.Pragma))
{
return null;
}
// We require 'public' and 's-max-age' or 'max-age' or the Expires header.
CacheControlHeaderValue cacheControl;
if (CacheControlHeaderValue.TryParse(cacheControlHeader, out cacheControl) && cacheControl.Public)
{
if (cacheControl.SharedMaxAge.HasValue)
{
return cacheControl.SharedMaxAge;
}
else if (cacheControl.MaxAge.HasValue)
{
return cacheControl.MaxAge;
}
DateTimeOffset expirationDate;
if (HeaderUtilities.TryParseDate(response.Headers[HeaderNames.Expires], out expirationDate))
{
var expiresOffset = expirationDate - DateTimeOffset.UtcNow;
if (expiresOffset > TimeSpan.Zero)
{
return expiresOffset;
}
}
}
return null;
}
}
}

View File

@ -78,6 +78,8 @@ namespace Microsoft.AspNet.Server.WebListener
}
}
internal bool EnableResponseCaching { get; set; } = true;
internal void Start(AppFunc app)
{
// Can't call Start twice
@ -160,7 +162,7 @@ namespace Microsoft.AspNet.Server.WebListener
try
{
Interlocked.Increment(ref _outstandingRequests);
FeatureContext featureContext = new FeatureContext(requestContext);
FeatureContext featureContext = new FeatureContext(requestContext, EnableResponseCaching);
await _appFunc(featureContext.Features).SupressContext();
requestContext.Dispose();
}

View File

@ -50,5 +50,11 @@ namespace Microsoft.AspNet.Server.WebListener
get { return _messagePump.MaxAccepts; }
set { _messagePump.MaxAccepts = value; }
}
public bool EnableResponseCaching
{
get { return _messagePump.EnableResponseCaching; }
set { _messagePump.EnableResponseCaching = value; }
}
}
}

View File

@ -5,6 +5,7 @@
"Microsoft.AspNet.Http.Features": "1.0.0-*",
"Microsoft.AspNet.Hosting.Server.Abstractions": "1.0.0-*",
"Microsoft.Framework.Logging.Abstractions": "1.0.0-*",
"Microsoft.Net.Http.Headers": "1.0.0-*",
"Microsoft.Net.Http.Server": "1.0.0-*"
},
"compilationOptions": {

View File

@ -212,7 +212,7 @@ namespace Microsoft.Net.Http.Server
internal static extern uint HttpReceiveHttpRequest(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_REQUEST* pRequestBuffer, uint requestBufferLength, uint* pBytesReturned, SafeNativeOverlapped pOverlapped);
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern uint HttpSendHttpResponse(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_RESPONSE_V2* pHttpResponse, void* pCachePolicy, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
internal static extern uint HttpSendHttpResponse(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_RESPONSE_V2* pHttpResponse, HTTP_CACHE_POLICY* pCachePolicy, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern uint HttpSendResponseEntityBody(SafeHandle requestQueueHandle, ulong requestId, uint flags, ushort entityChunkCount, HTTP_DATA_CHUNK* pEntityChunks, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
@ -369,6 +369,21 @@ namespace Microsoft.Net.Http.Server
internal ushort* pQueryString;
}
// Only cache unauthorized GETs + HEADs.
[StructLayout(LayoutKind.Sequential)]
internal struct HTTP_CACHE_POLICY
{
internal HTTP_CACHE_POLICY_TYPE Policy;
internal uint SecondsToLive;
}
internal enum HTTP_CACHE_POLICY_TYPE : int
{
HttpCachePolicyNocache = 0,
HttpCachePolicyUserInvalidates = 1,
HttpCachePolicyTimeToLive = 2,
}
[StructLayout(LayoutKind.Sequential)]
internal struct SOCKADDR
{

View File

@ -85,6 +85,7 @@ namespace Microsoft.Net.Http.Server
_bufferingEnabled = _requestContext.Server.BufferResponses;
_expectedBodyLength = 0;
_nativeStream = null;
CacheTtl = null;
}
private enum ResponseState
@ -306,6 +307,8 @@ namespace Microsoft.Net.Http.Server
get { return _responseState >= ResponseState.StartedSending; }
}
public TimeSpan? CacheTtl { get; set; }
private void EnsureResponseStream()
{
if (_nativeStream == null)
@ -391,6 +394,14 @@ namespace Microsoft.Net.Http.Server
_nativeResponse.Response_V1.pEntityChunks = null;
}
var cachePolicy = new HttpApi.HTTP_CACHE_POLICY();
var cacheTtl = CacheTtl;
if (cacheTtl.HasValue && cacheTtl.Value > TimeSpan.Zero)
{
cachePolicy.Policy = HttpApi.HTTP_CACHE_POLICY_TYPE.HttpCachePolicyTimeToLive;
cachePolicy.SecondsToLive = (uint)Math.Min(cacheTtl.Value.Ticks / TimeSpan.TicksPerSecond, Int32.MaxValue);
}
byte[] reasonPhraseBytes = new byte[HeaderEncoding.GetByteCount(reasonPhrase)];
fixed (byte* pReasonPhrase = reasonPhraseBytes)
{
@ -405,7 +416,7 @@ namespace Microsoft.Net.Http.Server
Request.RequestId,
(uint)flags,
pResponse,
null,
&cachePolicy,
&bytesSent,
SafeLocalFree.Zero,
0,

View File

@ -583,7 +583,6 @@ namespace Microsoft.Net.Http.Server
uint statusCode;
uint bytesSent = 0;
flags |= _leftToWrite == count ? HttpApi.HTTP_FLAGS.NONE : HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
bool startedSending = _requestContext.Response.HasStartedSending;
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration);
@ -602,6 +601,7 @@ namespace Microsoft.Net.Http.Server
bytesWritten = asyncResult.FileLength - offset;
}
// Update _leftToWrite now so we can queue up additional calls to SendFileAsync.
flags |= _leftToWrite == bytesWritten ? HttpApi.HTTP_FLAGS.NONE : HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
UpdateWritenCount((uint)bytesWritten);
try

View File

@ -0,0 +1,225 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.Http.Internal;
using Xunit;
namespace Microsoft.AspNet.Server.WebListener.FunctionalTests
{
public class ResponseCachingTests
{
[Fact]
public async Task Caching_NoCacheControl_NotCached()
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("2", await SendRequestAsync(address));
}
}
[Fact]
public async Task Caching_JustPublic_NotCached()
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Cache-Control"] = "public";
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("2", await SendRequestAsync(address));
}
}
[Fact]
public async Task Caching_MaxAge_Cached()
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("1", await SendRequestAsync(address));
}
}
[Fact]
public async Task Caching_SMaxAge_Cached()
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Cache-Control"] = "public, s-maxage=10";
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("1", await SendRequestAsync(address));
}
}
[Fact]
public async Task Caching_SMaxAgeAndMaxAge_SMaxAgePreferredCached()
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Cache-Control"] = "public, max-age=0, s-maxage=10";
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("1", await SendRequestAsync(address));
}
}
[Fact]
public async Task Caching_Expires_Cached()
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Cache-Control"] = "public";
httpContext.Response.Headers["Expires"] = (DateTime.UtcNow + TimeSpan.FromSeconds(10)).ToString("r");
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("1", await SendRequestAsync(address));
}
}
[Theory]
[InlineData("Set-cookie")]
[InlineData("vary")]
[InlineData("pragma")]
public async Task Caching_DisallowedResponseHeaders_NotCached(string headerName)
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
httpContext.Response.Headers[headerName] = "headerValue";
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("2", await SendRequestAsync(address));
}
}
[Theory]
[InlineData("0")]
[InlineData("-1")]
public async Task Caching_InvalidExpires_NotCached(string expiresValue)
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Cache-Control"] = "public";
httpContext.Response.Headers["Expires"] = expiresValue;
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("2", await SendRequestAsync(address));
}
}
[Fact]
public async Task Caching_ExpiresWithoutPublic_NotCached()
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Expires"] = (DateTime.UtcNow + TimeSpan.FromSeconds(10)).ToString("r");
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("2", await SendRequestAsync(address));
}
}
[Fact]
public async Task Caching_MaxAgeAndExpires_MaxAgePreferred()
{
var requestCount = 1;
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
httpContext.Response.Headers["Expires"] = (DateTime.UtcNow - TimeSpan.FromSeconds(10)).ToString("r"); // In the past
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
}))
{
Assert.Equal("1", await SendRequestAsync(address));
Assert.Equal("1", await SendRequestAsync(address));
}
}
private async Task<string> SendRequestAsync(string uri)
{
using (var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(10) })
{
var response = await client.GetAsync(uri);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(10, response.Content.Headers.ContentLength);
Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
return response.Headers.GetValues("x-request-count").FirstOrDefault();
}
}
}
}

View File

@ -13,5 +13,8 @@
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

File diff suppressed because it is too large Load Diff