Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@ internal sealed class DateHeaderValueManager : IHeartbeatHandler
// This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static
private static ReadOnlySpan<byte> DatePreambleBytes => "\r\nDate: "u8;

public TimeProvider _timeProvider;

private DateHeaderValues? _dateValues;

public DateHeaderValueManager(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}

/// <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
Expand All @@ -25,17 +32,17 @@ internal sealed class DateHeaderValueManager : IHeartbeatHandler
public DateHeaderValues GetDateHeaderValues() => _dateValues!;

// Called by the Timer (background) thread
public void OnHeartbeat(DateTimeOffset now)
public void OnHeartbeat()
{
SetDateValues(now);
SetDateValues();
}

/// <summary>
/// Sets date values from a provided ticks value
/// </summary>
/// <param name="value">A DateTimeOffset value</param>
private void SetDateValues(DateTimeOffset value)
private void SetDateValues()
{
var value = _timeProvider.GetUtcNow();
var dateValue = HeaderUtilities.FormatDate(value);
var dateBytes = new byte[DatePreambleBytes.Length + dateValue.Length];
DatePreambleBytes.CopyTo(dateBytes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ public Http1Connection(HttpConnectionContext context)

_context = context;
_parser = ServiceContext.HttpParser;
_keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks;
_requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks;
var frequency = context.ServiceContext.TimeProvider.TimestampFrequency;
_keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.ToTicks(frequency);
_requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.ToTicks(frequency);

_http1Output = new Http1OutputProducer(
_context.Transport.Output,
Expand Down Expand Up @@ -790,5 +791,5 @@ protected override Task TryProduceInvalidRequestResponse()
return base.TryProduceInvalidRequestResponse();
}

void IRequestProcessor.Tick(DateTimeOffset now) { }
void IRequestProcessor.Tick(long now) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ protected async Task OnConsumeAsyncAwaited()
{
Log.RequestBodyNotEntirelyRead(_context.ConnectionIdFeature, _context.TraceIdentifier);

_context.TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutReason.RequestBodyDrain);
_context.TimeoutControl.SetTimeout(
Constants.RequestBodyDrainTimeout.ToTicks(_context.ServiceContext.TimeProvider.TimestampFrequency), TimeoutReason.RequestBodyDrain);

try
{
Expand Down
24 changes: 12 additions & 12 deletions src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ internal sealed partial class Http2Connection : IHttp2StreamLifetimeHandler, IHt

private const int InitialStreamPoolSize = 5;
private const int MaxStreamPoolSize = 100;
private const long StreamPoolExpiryTicks = TimeSpan.TicksPerSecond * 5;
private const long StreamPoolExpirySeconds = 5;

private readonly HttpConnectionContext _context;
private readonly Http2FrameWriter _frameWriter;
Expand Down Expand Up @@ -111,7 +111,7 @@ public Http2Connection(HttpConnectionContext context)
_keepAlive = new Http2KeepAlive(
http2Limits.KeepAlivePingDelay,
http2Limits.KeepAlivePingTimeout,
context.ServiceContext.SystemClock);
context.ServiceContext.TimeProvider);
}

_serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection;
Expand Down Expand Up @@ -145,7 +145,7 @@ public Http2Connection(HttpConnectionContext context)

public KestrelTrace Log => _context.ServiceContext.Log;
public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures;
public ISystemClock SystemClock => _context.ServiceContext.SystemClock;
public TimeProvider TimeProvider => _context.ServiceContext.TimeProvider;
public ITimeoutControl TimeoutControl => _context.TimeoutControl;
public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits;

Expand Down Expand Up @@ -209,7 +209,7 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
ValidateTlsRequirements();

TimeoutControl.InitializeHttp2(_inputFlowControl);
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.ToTicks(TimeProvider.TimestampFrequency), TimeoutReason.KeepAlive);

if (!await TryReadPrefaceAsync())
{
Expand Down Expand Up @@ -240,7 +240,7 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
if (result.IsCanceled)
{
// Heartbeat will cancel ReadAsync and trigger expiring unused streams from pool.
StreamPool.RemoveExpired(SystemClock.UtcNowTicks);
StreamPool.RemoveExpired(TimeProvider.GetTimestamp());
}

try
Expand Down Expand Up @@ -729,7 +729,7 @@ private Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext> appli

if (!_incomingFrame.HeadersEndHeaders)
{
TimeoutControl.SetTimeout(Limits.RequestHeadersTimeout.Ticks, TimeoutReason.RequestHeaders);
TimeoutControl.SetTimeout(Limits.RequestHeadersTimeout.ToTicks(TimeProvider.TimestampFrequency), TimeoutReason.RequestHeaders);
}

// Start a new stream
Expand Down Expand Up @@ -944,7 +944,7 @@ private Task ProcessPingFrameAsync(in ReadOnlySequence<byte> payload)
// Incoming ping resets connection keep alive timeout
if (TimeoutControl.TimerReason == TimeoutReason.KeepAlive)
{
TimeoutControl.ResetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
TimeoutControl.ResetTimeout(Limits.KeepAliveTimeout.ToTicks(TimeProvider.TimestampFrequency), TimeoutReason.KeepAlive);
}

if (_incomingFrame.PingAck)
Expand Down Expand Up @@ -1241,7 +1241,7 @@ private void AbortStream(int streamId, IOException error)
}
}

void IRequestProcessor.Tick(DateTimeOffset now)
void IRequestProcessor.Tick(long now)
{
Input.CancelPendingRead();
}
Expand All @@ -1255,7 +1255,7 @@ void IHttp2StreamLifetimeHandler.OnStreamCompleted(Http2Stream stream)
private void UpdateCompletedStreams()
{
Http2Stream? firstRequedStream = null;
var now = SystemClock.UtcNowTicks;
var now = TimeProvider.GetTimestamp();

while (_completedStreams.TryDequeue(out var stream))
{
Expand All @@ -1270,7 +1270,7 @@ private void UpdateCompletedStreams()
if (stream.DrainExpirationTicks == default)
{
_serverActiveStreamCount--;
stream.DrainExpirationTicks = now + Constants.RequestBodyDrainTimeout.Ticks;
stream.DrainExpirationTicks = now + Constants.RequestBodyDrainTimeout.ToTicks(TimeProvider.TimestampFrequency);
}

if (stream.EndStreamReceived || stream.RstStreamReceived || stream.DrainExpirationTicks < now)
Expand Down Expand Up @@ -1304,7 +1304,7 @@ private void RemoveStream(Http2Stream stream)
// Pool and reuse the stream if it finished in a graceful state and there is space in the pool.

// This property is used to remove unused streams from the pool
stream.DrainExpirationTicks = SystemClock.UtcNowTicks + StreamPoolExpiryTicks;
stream.DrainExpirationTicks = TimeProvider.GetTimestamp() + StreamPoolExpirySeconds * TimeProvider.TimestampFrequency;

StreamPool.Push(stream);
}
Expand Down Expand Up @@ -1363,7 +1363,7 @@ private void UpdateConnectionState()
{
if (TimeoutControl.TimerReason == TimeoutReason.None)
{
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.ToTicks(TimeProvider.TimestampFrequency), TimeoutReason.KeepAlive);
}

// If we're awaiting headers, either a new stream will be started, or there will be a connection
Expand Down
34 changes: 16 additions & 18 deletions src/Servers/Kestrel/Core/src/Internal/Http2/Http2KeepAlive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;

Expand All @@ -19,31 +18,31 @@ internal sealed class Http2KeepAlive
// An empty ping payload
internal static readonly ReadOnlySequence<byte> PingPayload = new ReadOnlySequence<byte>(new byte[8]);

private readonly TimeSpan _keepAliveInterval;
private readonly TimeSpan _keepAliveTimeout;
private readonly ISystemClock _systemClock;
private readonly long _keepAliveInterval;
private readonly long _keepAliveTimeout;
private readonly TimeProvider _timeProvider;
private long _lastFrameReceivedTimestamp;
private long _pingSentTimestamp;

// Internal for testing
internal KeepAliveState _state;

public Http2KeepAlive(TimeSpan keepAliveInterval, TimeSpan keepAliveTimeout, ISystemClock systemClock)
public Http2KeepAlive(TimeSpan keepAliveInterval, TimeSpan keepAliveTimeout, TimeProvider timeProvider)
{
_keepAliveInterval = keepAliveInterval;
_keepAliveTimeout = keepAliveTimeout;
_systemClock = systemClock;
_keepAliveInterval = keepAliveInterval.ToTicks(timeProvider.TimestampFrequency);
_keepAliveTimeout = keepAliveTimeout == TimeSpan.MaxValue ? long.MaxValue
: keepAliveTimeout.ToTicks(timeProvider.TimestampFrequency);
_timeProvider = timeProvider;
}

public KeepAliveState ProcessKeepAlive(bool frameReceived)
{
var timestamp = _systemClock.UtcNowTicks;
var timestamp = _timeProvider.GetTimestamp();

if (frameReceived)
{
// System clock only has 1 second of precision, so the clock could be up to 1 second in the past.
// To err on the side of caution, add a second to the clock when calculating the ping sent time.
_lastFrameReceivedTimestamp = timestamp + TimeSpan.TicksPerSecond;
// To err on the side of caution, add a second to the time when calculating the ping sent time.
_lastFrameReceivedTimestamp = timestamp + _timeProvider.TimestampFrequency;

// Any frame received after the keep alive interval is exceeded resets the state back to none.
if (_state == KeepAliveState.PingSent)
Expand All @@ -58,23 +57,22 @@ public KeepAliveState ProcessKeepAlive(bool frameReceived)
{
case KeepAliveState.None:
// Check whether keep alive interval has passed since last frame received
if (timestamp > (_lastFrameReceivedTimestamp + _keepAliveInterval.Ticks))
if (timestamp > (_lastFrameReceivedTimestamp + _keepAliveInterval))
{
// Ping will be sent immeditely after this method finishes.
// Set the status directly to ping sent and set the timestamp
_state = KeepAliveState.PingSent;
// System clock only has 1 second of precision, so the clock could be up to 1 second in the past.
// To err on the side of caution, add a second to the clock when calculating the ping sent time.
_pingSentTimestamp = _systemClock.UtcNowTicks + TimeSpan.TicksPerSecond;
// To err on the side of caution, add a second to the time when calculating the ping sent time.
_pingSentTimestamp = timestamp + _timeProvider.TimestampFrequency;

// Indicate that the ping needs to be sent. This is only returned once
return KeepAliveState.SendPing;
}
break;
case KeepAliveState.PingSent:
if (_keepAliveTimeout != TimeSpan.MaxValue)
if (_keepAliveTimeout != long.MaxValue)
{
if (timestamp > (_pingSentTimestamp + _keepAliveTimeout.Ticks))
if (timestamp > (_pingSentTimestamp + _keepAliveTimeout))
{
_state = KeepAliveState.Timeout;
}
Expand Down
39 changes: 20 additions & 19 deletions src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode)
}
}

public void Tick(DateTimeOffset now)
public void Tick(long now)
{
if (_aborted)
{
Expand All @@ -203,10 +203,8 @@ public void Tick(DateTimeOffset now)
UpdateStreamTimeouts(now);
}

private void ValidateOpenControlStreams(DateTimeOffset now)
private void ValidateOpenControlStreams(long now)
{
var ticks = now.Ticks;

// This method validates that a connnection's control streams are open.
//
// They're checked on a delayed timer because when a connection is aborted or timed out, notifications are sent to open streams
Expand All @@ -216,10 +214,10 @@ private void ValidateOpenControlStreams(DateTimeOffset now)
//
// Realistically, control streams are never closed except when the connection is. A small delay in aborting the connection in the
// unlikely situation where a control stream is incorrectly closed should be fine.
ValidateOpenControlStream(OutboundControlStream, this, ticks);
ValidateOpenControlStream(ControlStream, this, ticks);
ValidateOpenControlStream(EncoderStream, this, ticks);
ValidateOpenControlStream(DecoderStream, this, ticks);
ValidateOpenControlStream(OutboundControlStream, this, now);
ValidateOpenControlStream(ControlStream, this, now);
ValidateOpenControlStream(EncoderStream, this, now);
ValidateOpenControlStream(DecoderStream, this, now);

static void ValidateOpenControlStream(Http3ControlStream? stream, Http3Connection connection, long ticks)
{
Expand All @@ -242,15 +240,16 @@ static void ValidateOpenControlStream(Http3ControlStream? stream, Http3Connectio
}
}

private void UpdateStreamTimeouts(DateTimeOffset now)
private void UpdateStreamTimeouts(long now)
{
// This method checks for timeouts:
// 1. When a stream first starts and waits to receive headers.
// Uses RequestHeadersTimeout.
// 2. When a stream finished and is waiting for underlying transport to drain.
// Uses MinResponseDataRate.

var ticks = now.Ticks;
var serviceContext = _context.ServiceContext;
var requestHeadersTimeout = serviceContext.ServerOptions.Limits.RequestHeadersTimeout.ToTicks(
serviceContext.TimeProvider.TimestampFrequency);

lock (_unidentifiedStreams)
{
Expand All @@ -259,11 +258,11 @@ private void UpdateStreamTimeouts(DateTimeOffset now)
if (stream.StreamTimeoutTicks == default)
{
// On expiration overflow, use max value.
var expirationTicks = ticks + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks;
var expirationTicks = now + requestHeadersTimeout;
stream.StreamTimeoutTicks = expirationTicks >= 0 ? expirationTicks : long.MaxValue;
}

if (stream.StreamTimeoutTicks < ticks)
if (stream.StreamTimeoutTicks < now)
{
stream.Abort(new("Stream timed out before its type was determined."));
}
Expand All @@ -279,11 +278,11 @@ private void UpdateStreamTimeouts(DateTimeOffset now)
if (stream.StreamTimeoutTicks == default)
{
// On expiration overflow, use max value.
var expirationTicks = ticks + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks;
var expirationTicks = now + requestHeadersTimeout;
stream.StreamTimeoutTicks = expirationTicks >= 0 ? expirationTicks : long.MaxValue;
}

if (stream.StreamTimeoutTicks < ticks)
if (stream.StreamTimeoutTicks < now)
{
if (stream.IsRequestStream)
{
Expand All @@ -305,10 +304,10 @@ private void UpdateStreamTimeouts(DateTimeOffset now)

if (stream.StreamTimeoutTicks == default)
{
stream.StreamTimeoutTicks = TimeoutControl.GetResponseDrainDeadline(ticks, minDataRate);
stream.StreamTimeoutTicks = TimeoutControl.GetResponseDrainDeadline(now, minDataRate);
}

if (stream.StreamTimeoutTicks < ticks)
if (stream.StreamTimeoutTicks < now)
{
// Cancel connection to be consistent with other data rate limits.
Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, stream.TraceIdentifier);
Expand Down Expand Up @@ -348,7 +347,8 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);

// Close the connection if we don't receive any request streams
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
TimeoutControl.SetTimeout(
Limits.KeepAliveTimeout.ToTicks(_context.ServiceContext.TimeProvider.TimestampFrequency), TimeoutReason.KeepAlive);

while (_stoppedAcceptingStreams == 0)
{
Expand Down Expand Up @@ -820,7 +820,8 @@ void IHttp3StreamLifetimeHandler.OnStreamCompleted(IHttp3Stream stream)

if (_activeRequestCount == 0)
{
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
TimeoutControl.SetTimeout(
Limits.KeepAliveTimeout.ToTicks(_context.ServiceContext.TimeProvider.TimestampFrequency), TimeoutReason.KeepAlive);
}
}
_streams.Remove(stream.StreamId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
public QPackDecoder QPackDecoder { get; private set; } = default!;

public PipeReader Input => _context.Transport.Input;
public ISystemClock SystemClock => _context.ServiceContext.SystemClock;
public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits;
public long StreamId => _streamIdFeature.StreamId;
public long StreamTimeoutTicks { get; set; }
Expand Down
Loading