diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln
index 3de96d251c..f82dbec103 100644
--- a/KestrelHttpServer.sln
+++ b/KestrelHttpServer.sln
@@ -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}"
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/DateHeaderValueManager.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/DateHeaderValueManager.cs
new file mode 100644
index 0000000000..216ce37f10
--- /dev/null
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/DateHeaderValueManager.cs
@@ -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
+{
+ ///
+ /// Manages the generation of the date header value.
+ ///
+ 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;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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
+ ///
+ /// The value.
+ 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");
+ }
+
+ ///
+ /// Releases all resources used by the current instance of .
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
index fdedb2ab19..bdd06735e6 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
@@ -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();
}
///
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/ISystemClock.cs b/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/ISystemClock.cs
new file mode 100644
index 0000000000..c741621de4
--- /dev/null
+++ b/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/ISystemClock.cs
@@ -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
+{
+ ///
+ /// Abstracts the system clock to facilitate testing.
+ ///
+ internal interface ISystemClock
+ {
+ ///
+ /// Retrieves the current system time in UTC.
+ ///
+ DateTimeOffset UtcNow { get; }
+ }
+}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/SystemClock.cs b/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/SystemClock.cs
new file mode 100644
index 0000000000..c16d40bf6a
--- /dev/null
+++ b/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/SystemClock.cs
@@ -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
+{
+ ///
+ /// Provides access to the normal system clock.
+ ///
+ internal class SystemClock : ISystemClock
+ {
+ ///
+ /// Retrieves the current system time in UTC.
+ ///
+ public DateTimeOffset UtcNow
+ {
+ get
+ {
+ return DateTimeOffset.UtcNow;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/ServerFactory.cs b/src/Microsoft.AspNet.Server.Kestrel/ServerFactory.cs
index 36c1ab3952..d81f34885f 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/ServerFactory.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/ServerFactory.cs
@@ -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();
- 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)
{
diff --git a/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs b/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs
index 8e915dccb9..8a29d2e380 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs
@@ -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; }
}
}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/project.json b/src/Microsoft.AspNet.Server.Kestrel/project.json
index 787728d2d4..7d3b95fbda 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/project.json
+++ b/src/Microsoft.AspNet.Server.Kestrel/project.json
@@ -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-*"
}
}
},
diff --git a/test/Microsoft.AspNet.Server.KestrelTests/DateHeaderValueManagerTests.cs b/test/Microsoft.AspNet.Server.KestrelTests/DateHeaderValueManagerTests.cs
new file mode 100644
index 0000000000..71c294caae
--- /dev/null
+++ b/test/Microsoft.AspNet.Server.KestrelTests/DateHeaderValueManagerTests.cs
@@ -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);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Server.KestrelTests/TestHelpers/MockSystemClock.cs b/test/Microsoft.AspNet.Server.KestrelTests/TestHelpers/MockSystemClock.cs
new file mode 100644
index 0000000000..6dbf6e3556
--- /dev/null
+++ b/test/Microsoft.AspNet.Server.KestrelTests/TestHelpers/MockSystemClock.cs
@@ -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; }
+ }
+}