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:
parent
5070b8073e
commit
7c46b2bd3b
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-*"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue