From 7c46b2bd3b005fbd92cdbaf5cb02c6108f39a15f Mon Sep 17 00:00:00 2001 From: damianedwards Date: Wed, 9 Sep 2015 09:54:02 -0700 Subject: [PATCH] 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 --- KestrelHttpServer.sln | 1 + .../Http/DateHeaderValueManager.cs | 145 ++++++++++++++++++ .../Http/Frame.cs | 2 +- .../Infrastructure/ISystemClock.cs | 18 +++ .../Infrastructure/SystemClock.cs | 24 +++ .../ServerFactory.cs | 10 +- .../ServiceContext.cs | 3 + .../project.json | 3 +- .../DateHeaderValueManagerTests.cs | 125 +++++++++++++++ .../TestHelpers/MockSystemClock.cs | 28 ++++ 10 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNet.Server.Kestrel/Http/DateHeaderValueManager.cs create mode 100644 src/Microsoft.AspNet.Server.Kestrel/Infrastructure/ISystemClock.cs create mode 100644 src/Microsoft.AspNet.Server.Kestrel/Infrastructure/SystemClock.cs create mode 100644 test/Microsoft.AspNet.Server.KestrelTests/DateHeaderValueManagerTests.cs create mode 100644 test/Microsoft.AspNet.Server.KestrelTests/TestHelpers/MockSystemClock.cs 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; } + } +}