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
|
build.cmd = build.cmd
|
||||||
global.json = global.json
|
global.json = global.json
|
||||||
makefile.shade = makefile.shade
|
makefile.shade = makefile.shade
|
||||||
|
NuGet.Config = NuGet.Config
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleApp", "samples\SampleApp\SampleApp.xproj", "{2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}"
|
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.Reset();
|
||||||
_responseHeaders.HeaderServer = "Kestrel";
|
_responseHeaders.HeaderServer = "Kestrel";
|
||||||
_responseHeaders.HeaderDate = DateTime.UtcNow.ToString("r");
|
_responseHeaders.HeaderDate = DateHeaderValueManager.GetDateHeaderValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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.Hosting.Server;
|
||||||
using Microsoft.AspNet.Http.Features;
|
using Microsoft.AspNet.Http.Features;
|
||||||
using Microsoft.AspNet.Server.Features;
|
using Microsoft.AspNet.Server.Features;
|
||||||
|
using Microsoft.AspNet.Server.Kestrel.Http;
|
||||||
using Microsoft.Dnx.Runtime;
|
using Microsoft.Dnx.Runtime;
|
||||||
using Microsoft.Framework.Configuration;
|
using Microsoft.Framework.Configuration;
|
||||||
using Microsoft.Framework.Logging;
|
using Microsoft.Framework.Logging;
|
||||||
|
|
@ -53,9 +54,16 @@ namespace Microsoft.AspNet.Server.Kestrel
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var information = (KestrelServerInformation)serverFeatures.Get<IKestrelServerInformation>();
|
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(engine);
|
||||||
|
disposables.Push(dateHeaderValueManager);
|
||||||
|
|
||||||
if (information.ThreadCount < 0)
|
if (information.ThreadCount < 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNet.Server.Kestrel
|
||||||
AppShutdown = context.AppShutdown;
|
AppShutdown = context.AppShutdown;
|
||||||
Memory = context.Memory;
|
Memory = context.Memory;
|
||||||
Log = context.Log;
|
Log = context.Log;
|
||||||
|
DateHeaderValueManager = context.DateHeaderValueManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IApplicationShutdown AppShutdown { get; set; }
|
public IApplicationShutdown AppShutdown { get; set; }
|
||||||
|
|
@ -26,5 +27,7 @@ namespace Microsoft.AspNet.Server.Kestrel
|
||||||
public IMemoryPool Memory { get; set; }
|
public IMemoryPool Memory { get; set; }
|
||||||
|
|
||||||
public IKestrelTrace Log { get; set; }
|
public IKestrelTrace Log { get; set; }
|
||||||
|
|
||||||
|
public DateHeaderValueManager DateHeaderValueManager { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@
|
||||||
"System.Threading": "4.0.11-beta-*",
|
"System.Threading": "4.0.11-beta-*",
|
||||||
"System.Threading.Tasks": "4.0.11-beta-*",
|
"System.Threading.Tasks": "4.0.11-beta-*",
|
||||||
"System.Threading.Thread": "4.0.0-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