Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Commit

Permalink
Add response minimum data rate feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
Cesar Blum Silveira authored Jul 8, 2017
1 parent 5185ebe commit eca4bfe
Show file tree
Hide file tree
Showing 30 changed files with 708 additions and 126 deletions.
3 changes: 3 additions & 0 deletions src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -342,4 +342,7 @@
<data name="SynchronousWritesDisallowed" xml:space="preserve">
<value>Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.</value>
</data>
<data name="PositiveNumberOrNullMinDataRateRequired" xml:space="preserve">
<value>Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
/// <summary>
/// Represents a minimum data rate for the request body of an HTTP request.
/// Feature to set the minimum data rate at which the the request body must be sent by the client.
/// </summary>
public interface IHttpMinRequestBodyDataRateFeature
{
/// <summary>
/// The minimum data rate in bytes/second at which the request body should be received.
/// The minimum data rate in bytes/second at which the request body must be sent by the client.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
/// <summary>
/// Feature to set the minimum data rate at which the response must be received by the client.
/// </summary>
public interface IHttpMinResponseDataRateFeature
{
/// <summary>
/// The minimum data rate in bytes/second at which the response must be received by the client.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
/// </summary>
MinDataRate MinDataRate { get; set; }
}
}
129 changes: 101 additions & 28 deletions src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
Expand All @@ -35,6 +35,10 @@ public class FrameConnection : IConnectionContext, ITimeoutControl
private long _readTimingElapsedTicks;
private long _readTimingBytesRead;

private object _writeTimingLock = new object();
private int _writeTimingWrites;
private long _writeTimingTimeoutTimestamp;

private Task _lifetimeTask;

public FrameConnection(FrameConnectionContext context)
Expand All @@ -46,7 +50,6 @@ public FrameConnection(FrameConnectionContext context)
internal Frame Frame => _frame;
internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton;


public bool TimedOut { get; private set; }

public string ConnectionId => _context.ConnectionId;
Expand Down Expand Up @@ -207,7 +210,6 @@ public void Timeout()
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");

TimedOut = true;
_readTimingEnabled = false;
_frame.Stop();
}

Expand Down Expand Up @@ -262,6 +264,20 @@ 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 (TimedOut)
{
return;
}

// TODO: Use PlatformApis.VolatileRead equivalent again
if (timestamp > Interlocked.Read(ref _timeoutTimestamp))
{
Expand All @@ -277,42 +293,67 @@ public void Tick(DateTimeOffset now)
Timeout();
}
}
else
}

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 (TimedOut || Interlocked.Read(ref _timeoutTimestamp) != long.MaxValue)
{
lock (_readTimingLock)
return;
}

lock (_readTimingLock)
{
if (_readTimingEnabled)
{
if (_readTimingEnabled)
{
// Reference in local var to avoid torn reads in case the min rate is changed via IHttpMinRequestBodyDataRateFeature
var minRequestBodyDataRate = _frame.MinRequestBodyDataRate;
// Reference in local var to avoid torn reads in case the min rate is changed via IHttpMinRequestBodyDataRateFeature
var minRequestBodyDataRate = _frame.MinRequestBodyDataRate;

_readTimingElapsedTicks += timestamp - _lastTimestamp;
_readTimingElapsedTicks += timestamp - _lastTimestamp;

if (minRequestBodyDataRate?.BytesPerSecond > 0 && _readTimingElapsedTicks > minRequestBodyDataRate.GracePeriod.Ticks)
{
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;

if (rate < minRequestBodyDataRate.BytesPerSecond && !Debugger.IsAttached)
{
Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, minRequestBodyDataRate.BytesPerSecond);
Timeout();
}
}
if (minRequestBodyDataRate?.BytesPerSecond > 0 && _readTimingElapsedTicks > minRequestBodyDataRate.GracePeriod.Ticks)
{
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;

// 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)
if (rate < minRequestBodyDataRate.BytesPerSecond && !Debugger.IsAttached)
{
_readTimingEnabled = false;
_readTimingPauseRequested = false;
Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, minRequestBodyDataRate.BytesPerSecond);
Timeout();
}
}

// 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;
}
}
}
}

Interlocked.Exchange(ref _lastTimestamp, timestamp);
private void CheckForWriteDataRateTimeout(long timestamp)
{
if (TimedOut)
{
return;
}

lock (_writeTimingLock)
{
if (_writeTimingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
{
TimedOut = true;
Log.ResponseMininumDataRateNotSatisfied(_frame.ConnectionIdFeature, _frame.TraceIdentifier);
Abort(new TimeoutException());
}
}
}

public void SetTimeout(long ticks, TimeoutAction timeoutAction)
Expand Down Expand Up @@ -381,5 +422,37 @@ public void BytesRead(int count)
{
Interlocked.Add(ref _readTimingBytesRead, count);
}

public void StartTimingWrite(int size)
{
lock (_writeTimingLock)
{
var minResponseDataRate = _frame.MinResponseDataRate;

if (minResponseDataRate != null)
{
var timeoutTicks = Math.Max(
minResponseDataRate.GracePeriod.Ticks,
TimeSpan.FromSeconds(size / minResponseDataRate.BytesPerSecond).Ticks);

if (_writeTimingWrites == 0)
{
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
_writeTimingTimeoutTimestamp = _lastTimestamp + Heartbeat.Interval.Ticks;
}

_writeTimingTimeoutTimestamp += timeoutTicks;
_writeTimingWrites++;
}
}
}

public void StopTimingWrite()
{
lock (_writeTimingLock)
{
_writeTimingWrites--;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public partial class Frame : IFeatureCollection,
IHttpRequestIdentifierFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature,
IHttpMinRequestBodyDataRateFeature
IHttpMinRequestBodyDataRateFeature,
IHttpMinResponseDataRateFeature
{
// NOTE: When feature interfaces are added to or removed from this Frame class implementation,
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
Expand Down Expand Up @@ -242,6 +243,12 @@ MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
set => MinRequestBodyDataRate = value;
}

MinDataRate IHttpMinResponseDataRateFeature.MinDataRate
{
get => MinResponseDataRate;
set => MinResponseDataRate = value;
}

object IFeatureCollection.this[Type key]
{
get => FastFeatureGet(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public partial class Frame
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
private static readonly Type IHttpMaxRequestBodySizeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
private static readonly Type IHttpMinRequestBodyDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature);
private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinResponseDataRateFeature);
private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);

Expand All @@ -45,6 +46,7 @@ public partial class Frame
private object _currentISessionFeature;
private object _currentIHttpMaxRequestBodySizeFeature;
private object _currentIHttpMinRequestBodyDataRateFeature;
private object _currentIHttpMinResponseDataRateFeature;
private object _currentIHttpBodyControlFeature;
private object _currentIHttpSendFileFeature;

Expand All @@ -58,6 +60,7 @@ private void FastReset()
_currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpMinRequestBodyDataRateFeature = this;
_currentIHttpMinResponseDataRateFeature = this;
_currentIHttpBodyControlFeature = this;

_currentIServiceProvidersFeature = null;
Expand Down Expand Up @@ -142,6 +145,10 @@ internal object FastFeatureGet(Type key)
{
return _currentIHttpMinRequestBodyDataRateFeature;
}
if (key == IHttpMinResponseDataRateFeatureType)
{
return _currentIHttpMinResponseDataRateFeature;
}
if (key == IHttpBodyControlFeatureType)
{
return _currentIHttpBodyControlFeature;
Expand Down Expand Up @@ -242,6 +249,11 @@ internal void FastFeatureSet(Type key, object feature)
_currentIHttpMinRequestBodyDataRateFeature = feature;
return;
}
if (key == IHttpMinResponseDataRateFeatureType)
{
_currentIHttpMinResponseDataRateFeature = feature;
return;
}
if (key == IHttpBodyControlFeatureType)
{
_currentIHttpBodyControlFeature = feature;
Expand Down Expand Up @@ -325,6 +337,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
{
yield return new KeyValuePair<Type, object>(IHttpMinRequestBodyDataRateFeatureType, _currentIHttpMinRequestBodyDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature);
}
if (_currentIHttpMinResponseDataRateFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpMinResponseDataRateFeatureType, _currentIHttpMinResponseDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinResponseDataRateFeature);
}
if (_currentIHttpBodyControlFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public Frame(FrameContext frameContext)
_keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks;
_requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks;

Output = new OutputProducer(frameContext.Output, frameContext.ConnectionId, frameContext.ServiceContext.Log);
Output = new OutputProducer(frameContext.Output, frameContext.ConnectionId, frameContext.ServiceContext.Log, TimeoutControl);
RequestBodyPipe = CreateRequestBodyPipe();
}

Expand Down Expand Up @@ -302,6 +302,8 @@ private CancellationTokenSource RequestAbortedSource

public MinDataRate MinRequestBodyDataRate { get; set; }

public MinDataRate MinResponseDataRate { get; set; }

public void InitializeStreams(MessageBody messageBody)
{
if (_frameStreams == null)
Expand Down Expand Up @@ -381,6 +383,7 @@ public void Reset()
_requestCount++;

MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
MinResponseDataRate = ServerOptions.Limits.MinResponseDataRate;
}

/// <summary>
Expand Down Expand Up @@ -418,7 +421,7 @@ public void Abort(Exception error)

_frameStreams?.Abort(error);

Output.Abort();
Output.Abort(error);

// Potentially calling user code. CancelRequestAbortedToken logs any exceptions.
ServiceContext.ThreadPool.UnsafeRun(state => ((Frame)state).CancelRequestAbortedToken(), this);
Expand Down
Loading

0 comments on commit eca4bfe

Please sign in to comment.