Use a timer to generate the value for the Date header in responses:

- Doing it on each request is expensive
- The Timer is started when the first request comes in and fires every second
- Every request flips a bool so the Timer knows requests are coming in
- The Timer stops itself after a period of no requests coming in (10 seconds)
- #163
This commit is contained in:
damianedwards 2015-09-09 09:54:02 -07:00
parent 5070b8073e
commit 7c46b2bd3b
10 changed files with 356 additions and 3 deletions

View File

@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
build.cmd = build.cmd
global.json = global.json
makefile.shade = makefile.shade
NuGet.Config = NuGet.Config
EndProjectSection
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleApp", "samples\SampleApp\SampleApp.xproj", "{2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}"

View File

@ -0,0 +1,145 @@
// 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.Threading;
using Microsoft.AspNet.Server.Kestrel.Infrastructure;
namespace Microsoft.AspNet.Server.Kestrel.Http
{
/// <summary>
/// Manages the generation of the date header value.
/// </summary>
public class DateHeaderValueManager : IDisposable
{
private readonly ISystemClock _systemClock;
private readonly TimeSpan _timeWithoutRequestsUntilIdle;
private readonly TimeSpan _timerInterval;
private volatile string _dateValue;
private object _timerLocker = new object();
private bool _isDisposed = false;
private bool _hadRequestsSinceLastTimerTick = false;
private Timer _dateValueTimer;
private DateTimeOffset _lastRequestSeen = DateTimeOffset.MinValue;
/// <summary>
/// Initializes a new instance of the <see cref="DateHeaderValueManager"/> class.
/// </summary>
public DateHeaderValueManager()
: this(
systemClock: new SystemClock(),
timeWithoutRequestsUntilIdle: TimeSpan.FromSeconds(10),
timerInterval: TimeSpan.FromSeconds(1))
{
}
// Internal for testing
internal DateHeaderValueManager(
ISystemClock systemClock,
TimeSpan timeWithoutRequestsUntilIdle,
TimeSpan timerInterval)
{
_systemClock = systemClock;
_timeWithoutRequestsUntilIdle = timeWithoutRequestsUntilIdle;
_timerInterval = timerInterval;
}
/// <summary>
/// Returns a value representing the current server date/time for use in the HTTP "Date" response header
/// in accordance with http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18
/// </summary>
/// <returns>The value.</returns>
public string GetDateHeaderValue()
{
PumpTimer();
// See https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx#RFC1123 for info on the format
// string used here.
// The null-coalesce here is to protect against returning null after Dispose() is called, at which
// point _dateValue will be null forever after.
return _dateValue ?? _systemClock.UtcNow.ToString("r");
}
/// <summary>
/// Releases all resources used by the current instance of <see cref="DateHeaderValueManager"/>.
/// </summary>
public void Dispose()
{
lock (_timerLocker)
{
if (_dateValueTimer != null)
{
DisposeTimer();
}
_isDisposed = true;
}
}
private void PumpTimer()
{
_hadRequestsSinceLastTimerTick = true;
// If we're already disposed we don't care about starting the timer again. This avoids us having to worry
// about requests in flight during dispose (not that that should actually happen) as those will just get
// SystemClock.UtcNow (aka "the slow way").
if (!_isDisposed && _dateValueTimer == null)
{
lock (_timerLocker)
{
if (!_isDisposed && _dateValueTimer == null)
{
// Immediately assign the date value and start the timer again. We assign the value immediately
// here as the timer won't fire until the timer interval has passed and we want a value assigned
// inline now to serve requests that occur in the meantime.
_dateValue = _systemClock.UtcNow.ToString("r");
_dateValueTimer = new Timer(UpdateDateValue, state: null, dueTime: _timerInterval, period: _timerInterval);
}
}
}
}
// Called by the Timer (background) thread
private void UpdateDateValue(object state)
{
var now = _systemClock.UtcNow;
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 for required format of Date header
_dateValue = now.ToString("r");
if (_hadRequestsSinceLastTimerTick)
{
// We served requests since the last tick, reset the flag and return as we're still active
_hadRequestsSinceLastTimerTick = false;
_lastRequestSeen = now;
return;
}
// No requests since the last timer tick, we need to check if we're beyond the idle threshold
var timeSinceLastRequestSeen = now - _lastRequestSeen;
if (timeSinceLastRequestSeen >= _timeWithoutRequestsUntilIdle)
{
// No requests since idle threshold so stop the timer if it's still running
if (_dateValueTimer != null)
{
lock (_timerLocker)
{
if (_dateValueTimer != null)
{
DisposeTimer();
}
}
}
}
}
private void DisposeTimer()
{
_dateValueTimer.Dispose();
_dateValueTimer = null;
_dateValue = null;
}
}
}

View File

@ -101,7 +101,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
_responseHeaders.Reset();
_responseHeaders.HeaderServer = "Kestrel";
_responseHeaders.HeaderDate = DateTime.UtcNow.ToString("r");
_responseHeaders.HeaderDate = DateHeaderValueManager.GetDateHeaderValue();
}
/// <summary>

View File

@ -0,0 +1,18 @@
// 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;
namespace Microsoft.AspNet.Server.Kestrel.Infrastructure
{
/// <summary>
/// Abstracts the system clock to facilitate testing.
/// </summary>
internal interface ISystemClock
{
/// <summary>
/// Retrieves the current system time in UTC.
/// </summary>
DateTimeOffset UtcNow { get; }
}
}

View File

@ -0,0 +1,24 @@
// 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;
namespace Microsoft.AspNet.Server.Kestrel.Infrastructure
{
/// <summary>
/// Provides access to the normal system clock.
/// </summary>
internal class SystemClock : ISystemClock
{
/// <summary>
/// Retrieves the current system time in UTC.
/// </summary>
public DateTimeOffset UtcNow
{
get
{
return DateTimeOffset.UtcNow;
}
}
}
}

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Hosting.Server;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Server.Features;
using Microsoft.AspNet.Server.Kestrel.Http;
using Microsoft.Dnx.Runtime;
using Microsoft.Framework.Configuration;
using Microsoft.Framework.Logging;
@ -53,9 +54,16 @@ namespace Microsoft.AspNet.Server.Kestrel
try
{
var information = (KestrelServerInformation)serverFeatures.Get<IKestrelServerInformation>();
var engine = new KestrelEngine(_libraryManager, new ServiceContext { AppShutdown = _appShutdownService, Log = new KestrelTrace(_logger) });
var dateHeaderValueManager = new DateHeaderValueManager();
var engine = new KestrelEngine(_libraryManager, new ServiceContext
{
AppShutdown = _appShutdownService,
Log = new KestrelTrace(_logger),
DateHeaderValueManager = dateHeaderValueManager
});
disposables.Push(engine);
disposables.Push(dateHeaderValueManager);
if (information.ThreadCount < 0)
{

View File

@ -19,6 +19,7 @@ namespace Microsoft.AspNet.Server.Kestrel
AppShutdown = context.AppShutdown;
Memory = context.Memory;
Log = context.Log;
DateHeaderValueManager = context.DateHeaderValueManager;
}
public IApplicationShutdown AppShutdown { get; set; }
@ -26,5 +27,7 @@ namespace Microsoft.AspNet.Server.Kestrel
public IMemoryPool Memory { get; set; }
public IKestrelTrace Log { get; set; }
public DateHeaderValueManager DateHeaderValueManager { get; set; }
}
}

View File

@ -33,7 +33,8 @@
"System.Threading": "4.0.11-beta-*",
"System.Threading.Tasks": "4.0.11-beta-*",
"System.Threading.Thread": "4.0.0-beta-*",
"System.Threading.ThreadPool": "4.0.10-beta-*"
"System.Threading.ThreadPool": "4.0.10-beta-*",
"System.Threading.Timer": "4.0.1-beta-*"
}
}
},

View File

@ -0,0 +1,125 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Server.Kestrel.Http;
using Microsoft.AspNet.Server.KestrelTests.TestHelpers;
using Xunit;
namespace Microsoft.AspNet.Server.KestrelTests
{
public class DateHeaderValueManagerTests
{
[Fact]
public void GetDateHeaderValue_ReturnsDateValueInRFC1123Format()
{
var now = DateTimeOffset.UtcNow;
var systemClock = new MockSystemClock
{
UtcNow = now
};
var timeWithoutRequestsUntilIdle = TimeSpan.FromSeconds(1);
var timerInterval = TimeSpan.FromSeconds(10);
var dateHeaderValueManager = new DateHeaderValueManager(systemClock, timeWithoutRequestsUntilIdle, timerInterval);
string result;
try
{
result = dateHeaderValueManager.GetDateHeaderValue();
}
finally
{
dateHeaderValueManager.Dispose();
}
Assert.Equal(now.ToString("r"), result);
}
[Fact]
public void GetDateHeaderValue_ReturnsCachedValueBetweenTimerTicks()
{
var now = DateTimeOffset.UtcNow;
var future = now.AddSeconds(10);
var systemClock = new MockSystemClock
{
UtcNow = now
};
var timeWithoutRequestsUntilIdle = TimeSpan.FromSeconds(1);
var timerInterval = TimeSpan.FromSeconds(10);
var dateHeaderValueManager = new DateHeaderValueManager(systemClock, timeWithoutRequestsUntilIdle, timerInterval);
string result1;
string result2;
try
{
result1 = dateHeaderValueManager.GetDateHeaderValue();
systemClock.UtcNow = future;
result2 = dateHeaderValueManager.GetDateHeaderValue();
}
finally
{
dateHeaderValueManager.Dispose();
}
Assert.Equal(now.ToString("r"), result1);
Assert.Equal(now.ToString("r"), result2);
Assert.Equal(1, systemClock.UtcNowCalled);
}
[Fact]
public async Task GetDateHeaderValue_ReturnsUpdatedValueAfterIdle()
{
var now = DateTimeOffset.UtcNow;
var future = now.AddSeconds(10);
var systemClock = new MockSystemClock
{
UtcNow = now
};
var timeWithoutRequestsUntilIdle = TimeSpan.FromMilliseconds(50);
var timerInterval = TimeSpan.FromMilliseconds(10);
var dateHeaderValueManager = new DateHeaderValueManager(systemClock, timeWithoutRequestsUntilIdle, timerInterval);
string result1;
string result2;
try
{
result1 = dateHeaderValueManager.GetDateHeaderValue();
systemClock.UtcNow = future;
// Wait for twice the idle timeout to ensure the timer is stopped
await Task.Delay(timeWithoutRequestsUntilIdle.Add(timeWithoutRequestsUntilIdle));
result2 = dateHeaderValueManager.GetDateHeaderValue();
}
finally
{
dateHeaderValueManager.Dispose();
}
Assert.Equal(now.ToString("r"), result1);
Assert.Equal(future.ToString("r"), result2);
Assert.True(systemClock.UtcNowCalled >= 2);
}
[Fact]
public void GetDateHeaderValue_ReturnsDateValueAfterDisposed()
{
var now = DateTimeOffset.UtcNow;
var future = now.AddSeconds(10);
var systemClock = new MockSystemClock
{
UtcNow = now
};
var timeWithoutRequestsUntilIdle = TimeSpan.FromSeconds(1);
var timerInterval = TimeSpan.FromSeconds(10);
var dateHeaderValueManager = new DateHeaderValueManager(systemClock, timeWithoutRequestsUntilIdle, timerInterval);
var result1 = dateHeaderValueManager.GetDateHeaderValue();
dateHeaderValueManager.Dispose();
systemClock.UtcNow = future;
var result2 = dateHeaderValueManager.GetDateHeaderValue();
Assert.Equal(now.ToString("r"), result1);
Assert.Equal(future.ToString("r"), result2);
}
}
}

View File

@ -0,0 +1,28 @@
// 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 Microsoft.AspNet.Server.Kestrel.Infrastructure;
namespace Microsoft.AspNet.Server.KestrelTests.TestHelpers
{
public class MockSystemClock : ISystemClock
{
private DateTimeOffset _utcNow = DateTimeOffset.Now;
public DateTimeOffset UtcNow
{
get
{
UtcNowCalled++;
return _utcNow;
}
set
{
_utcNow = value;
}
}
public int UtcNowCalled { get; private set; }
}
}