313 lines
11 KiB
C#
313 lines
11 KiB
C#
// 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.Diagnostics;
|
|
using System.Threading;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
|
|
|
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|
{
|
|
public class TimeoutControl : ITimeoutControl, IConnectionTimeoutFeature
|
|
{
|
|
private readonly ITimeoutHandler _timeoutHandler;
|
|
|
|
private long _lastTimestamp;
|
|
private long _timeoutTimestamp = long.MaxValue;
|
|
|
|
private readonly object _readTimingLock = new object();
|
|
private MinDataRate _minReadRate;
|
|
private bool _readTimingEnabled;
|
|
private bool _readTimingPauseRequested;
|
|
private long _readTimingElapsedTicks;
|
|
private long _readTimingBytesRead;
|
|
private InputFlowControl _connectionInputFlowControl;
|
|
// The following are always 0 or 1 for HTTP/1.x
|
|
private int _concurrentIncompleteRequestBodies;
|
|
private int _concurrentAwaitingReads;
|
|
|
|
private readonly object _writeTimingLock = new object();
|
|
private int _concurrentAwaitingWrites;
|
|
private long _writeTimingTimeoutTimestamp;
|
|
|
|
public TimeoutControl(ITimeoutHandler timeoutHandler)
|
|
{
|
|
_timeoutHandler = timeoutHandler;
|
|
}
|
|
|
|
public TimeoutReason TimerReason { get; private set; }
|
|
|
|
internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton;
|
|
|
|
public void Initialize(DateTimeOffset now)
|
|
{
|
|
_lastTimestamp = now.Ticks;
|
|
}
|
|
|
|
public void Tick(DateTimeOffset now)
|
|
{
|
|
var timestamp = now.Ticks;
|
|
|
|
CheckForTimeout(timestamp);
|
|
CheckForReadDataRateTimeout(timestamp);
|
|
CheckForWriteDataRateTimeout(timestamp);
|
|
|
|
Interlocked.Exchange(ref _lastTimestamp, timestamp);
|
|
}
|
|
|
|
private void CheckForTimeout(long timestamp)
|
|
{
|
|
if (!Debugger.IsAttached)
|
|
{
|
|
if (timestamp > Interlocked.Read(ref _timeoutTimestamp))
|
|
{
|
|
var timeoutReason = TimerReason;
|
|
|
|
CancelTimeout();
|
|
|
|
_timeoutHandler.OnTimeout(timeoutReason);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CheckForReadDataRateTimeout(long timestamp)
|
|
{
|
|
// The only time when both a timeout is set and the read data rate could be enforced is
|
|
// when draining the request body. Since there's already a (short) timeout set for draining,
|
|
// it's safe to not check the data rate at this point.
|
|
if (Interlocked.Read(ref _timeoutTimestamp) != long.MaxValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Don't enforce the rate timeout if there is back pressure due to HTTP/2 connection-level input
|
|
// flow control. We don't consider stream-level flow control, because we wouldn't be timing a read
|
|
// for any stream that didn't have a completely empty stream-level flow control window.
|
|
if (_connectionInputFlowControl?.IsAvailabilityLow == true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_readTimingLock)
|
|
{
|
|
if (!_readTimingEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Assume overly long tick intervals are the result of server resource starvation.
|
|
// Don't count extra time between ticks against the rate limit.
|
|
_readTimingElapsedTicks += Math.Min(timestamp - _lastTimestamp, Heartbeat.Interval.Ticks);
|
|
|
|
if (_minReadRate.BytesPerSecond > 0 && _readTimingElapsedTicks > _minReadRate.GracePeriod.Ticks)
|
|
{
|
|
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
|
|
var rate = _readTimingBytesRead / elapsedSeconds;
|
|
|
|
if (rate < _minReadRate.BytesPerSecond && !Debugger.IsAttached)
|
|
{
|
|
_timeoutHandler.OnTimeout(TimeoutReason.ReadDataRate);
|
|
}
|
|
}
|
|
|
|
// PauseTimingReads() cannot just set _timingReads to false. It needs to go through at least one tick
|
|
// before pausing, otherwise _readTimingElapsed might never be updated if PauseTimingReads() is always
|
|
// called before the next tick.
|
|
if (_readTimingPauseRequested)
|
|
{
|
|
_readTimingEnabled = false;
|
|
_readTimingPauseRequested = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CheckForWriteDataRateTimeout(long timestamp)
|
|
{
|
|
lock (_writeTimingLock)
|
|
{
|
|
// Assume overly long tick intervals are the result of server resource starvation.
|
|
// Don't count extra time between ticks against the rate limit.
|
|
var extraTimeForTick = timestamp - _lastTimestamp - Heartbeat.Interval.Ticks;
|
|
|
|
if (extraTimeForTick > 0)
|
|
{
|
|
_writeTimingTimeoutTimestamp += extraTimeForTick;
|
|
}
|
|
|
|
if (_concurrentAwaitingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
|
|
{
|
|
_timeoutHandler.OnTimeout(TimeoutReason.WriteDataRate);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SetTimeout(long ticks, TimeoutReason timeoutReason)
|
|
{
|
|
Debug.Assert(_timeoutTimestamp == long.MaxValue, "Concurrent timeouts are not supported.");
|
|
|
|
AssignTimeout(ticks, timeoutReason);
|
|
}
|
|
|
|
public void ResetTimeout(long ticks, TimeoutReason timeoutReason)
|
|
{
|
|
AssignTimeout(ticks, timeoutReason);
|
|
}
|
|
|
|
public void CancelTimeout()
|
|
{
|
|
Interlocked.Exchange(ref _timeoutTimestamp, long.MaxValue);
|
|
|
|
TimerReason = TimeoutReason.None;
|
|
}
|
|
|
|
private void AssignTimeout(long ticks, TimeoutReason timeoutReason)
|
|
{
|
|
TimerReason = timeoutReason;
|
|
|
|
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
|
|
Interlocked.Exchange(ref _timeoutTimestamp, Interlocked.Read(ref _lastTimestamp) + ticks + Heartbeat.Interval.Ticks);
|
|
}
|
|
|
|
public void InitializeHttp2(InputFlowControl connectionInputFlowControl)
|
|
{
|
|
_connectionInputFlowControl = connectionInputFlowControl;
|
|
}
|
|
|
|
public void StartRequestBody(MinDataRate minRate)
|
|
{
|
|
lock (_readTimingLock)
|
|
{
|
|
// minRate is always KestrelServerLimits.MinRequestBodyDataRate for HTTP/2 which is the only protocol that supports concurrent request bodies.
|
|
Debug.Assert(_concurrentIncompleteRequestBodies == 0 || minRate == _minReadRate, "Multiple simultaneous read data rates are not supported.");
|
|
|
|
_minReadRate = minRate;
|
|
_concurrentIncompleteRequestBodies++;
|
|
|
|
if (_concurrentIncompleteRequestBodies == 1)
|
|
{
|
|
_readTimingElapsedTicks = 0;
|
|
_readTimingBytesRead = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void StopRequestBody()
|
|
{
|
|
lock (_readTimingLock)
|
|
{
|
|
_concurrentIncompleteRequestBodies--;
|
|
|
|
if (_concurrentIncompleteRequestBodies == 0)
|
|
{
|
|
_readTimingEnabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void StopTimingRead()
|
|
{
|
|
lock (_readTimingLock)
|
|
{
|
|
_concurrentAwaitingReads--;
|
|
|
|
if (_concurrentAwaitingReads == 0)
|
|
{
|
|
_readTimingPauseRequested = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void StartTimingRead()
|
|
{
|
|
lock (_readTimingLock)
|
|
{
|
|
_concurrentAwaitingReads++;
|
|
|
|
_readTimingEnabled = true;
|
|
|
|
// In case pause and resume were both called between ticks
|
|
_readTimingPauseRequested = false;
|
|
}
|
|
}
|
|
|
|
public void BytesRead(long count)
|
|
{
|
|
Debug.Assert(count >= 0, "BytesRead count must not be negative.");
|
|
|
|
lock (_readTimingLock)
|
|
{
|
|
_readTimingBytesRead += count;
|
|
}
|
|
}
|
|
|
|
public void StartTimingWrite()
|
|
{
|
|
lock (_writeTimingLock)
|
|
{
|
|
_concurrentAwaitingWrites++;
|
|
}
|
|
}
|
|
|
|
public void StopTimingWrite()
|
|
{
|
|
lock (_writeTimingLock)
|
|
{
|
|
_concurrentAwaitingWrites--;
|
|
}
|
|
}
|
|
|
|
public void BytesWrittenToBuffer(MinDataRate minRate, long count)
|
|
{
|
|
lock (_writeTimingLock)
|
|
{
|
|
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
|
|
var currentTimeUpperBound = Interlocked.Read(ref _lastTimestamp) + Heartbeat.Interval.Ticks;
|
|
var ticksToCompleteWriteAtMinRate = TimeSpan.FromSeconds(count / minRate.BytesPerSecond).Ticks;
|
|
|
|
// If ticksToCompleteWriteAtMinRate is less than the configured grace period,
|
|
// allow that write to take up to the grace period to complete. Only add the grace period
|
|
// to the current time and not to any accumulated timeout.
|
|
var singleWriteTimeoutTimestamp = currentTimeUpperBound + Math.Max(
|
|
minRate.GracePeriod.Ticks,
|
|
ticksToCompleteWriteAtMinRate);
|
|
|
|
// Don't penalize a connection for completing previous writes more quickly than required.
|
|
// We don't want to kill a connection when flushing the chunk terminator just because the previous
|
|
// chunk was large if the previous chunk was flushed quickly.
|
|
|
|
// Don't add any grace period to this accumulated timeout because the grace period could
|
|
// get accumulated repeatedly making the timeout for a bunch of consecutive small writes
|
|
// far too conservative.
|
|
var accumulatedWriteTimeoutTimestamp = _writeTimingTimeoutTimestamp + ticksToCompleteWriteAtMinRate;
|
|
|
|
_writeTimingTimeoutTimestamp = Math.Max(singleWriteTimeoutTimestamp, accumulatedWriteTimeoutTimestamp);
|
|
}
|
|
}
|
|
|
|
void IConnectionTimeoutFeature.SetTimeout(TimeSpan timeSpan)
|
|
{
|
|
if (timeSpan < TimeSpan.Zero)
|
|
{
|
|
throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan));
|
|
}
|
|
if (_timeoutTimestamp != long.MaxValue)
|
|
{
|
|
throw new InvalidOperationException(CoreStrings.ConcurrentTimeoutsNotSupported);
|
|
}
|
|
|
|
SetTimeout(timeSpan.Ticks, TimeoutReason.TimeoutFeature);
|
|
}
|
|
|
|
void IConnectionTimeoutFeature.ResetTimeout(TimeSpan timeSpan)
|
|
{
|
|
if (timeSpan < TimeSpan.Zero)
|
|
{
|
|
throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan));
|
|
}
|
|
|
|
ResetTimeout(timeSpan.Ticks, TimeoutReason.TimeoutFeature);
|
|
}
|
|
}
|
|
}
|