Skip to content

Commit fcc04f8

Browse files
author
Cesar Blum Silveira
authored
Add request body minimum data rate feature (#1874).
1 parent f96c48c commit fcc04f8

33 files changed

+1381
-184
lines changed

KestrelHttpServer.sln

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{0EF2AC
2525
test\shared\KestrelTestLoggerProvider.cs = test\shared\KestrelTestLoggerProvider.cs
2626
test\shared\LifetimeNotImplemented.cs = test\shared\LifetimeNotImplemented.cs
2727
test\shared\MockConnectionInformation.cs = test\shared\MockConnectionInformation.cs
28-
test\shared\MockFrameControl.cs = test\shared\MockFrameControl.cs
2928
test\shared\MockLogger.cs = test\shared\MockLogger.cs
3029
test\shared\MockSystemClock.cs = test\shared\MockSystemClock.cs
3130
test\shared\StringExtensions.cs = test\shared\StringExtensions.cs

src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -327,4 +327,10 @@
327327
<data name="MaxRequestBodySizeCannotBeModifiedForUpgradedRequests" xml:space="preserve">
328328
<value>The maximum request body size cannot be modified after the request has been upgraded.</value>
329329
</data>
330-
</root>
330+
<data name="PositiveTimeSpanRequired" xml:space="preserve">
331+
<value>Value must be a positive TimeSpan.</value>
332+
</data>
333+
<data name="NonNegativeTimeSpanRequired" xml:space="preserve">
334+
<value>Value must be a non-negative TimeSpan.</value>
335+
</data>
336+
</root>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
5+
{
6+
/// <summary>
7+
/// Represents a minimum data rate for the request body of an HTTP request.
8+
/// </summary>
9+
public interface IHttpRequestBodyMinimumDataRateFeature
10+
{
11+
/// <summary>
12+
/// The minimum data rate in bytes/second at which the request body should be received.
13+
/// Setting this property to null indicates no minimum data rate should be enforced.
14+
/// This limit has no effect on upgraded connections which are always unlimited.
15+
/// </summary>
16+
MinimumDataRate MinimumDataRate { get; set; }
17+
}
18+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
7+
{
8+
public class MinimumDataRate
9+
{
10+
/// <summary>
11+
/// Creates a new instance of <see cref="MinimumDataRate"/>.
12+
/// </summary>
13+
/// <param name="rate">The minimum rate in bytes/second at which data should be processed.</param>
14+
/// <param name="gracePeriod">The amount of time to delay enforcement of <paramref name="rate"/>.</param>
15+
public MinimumDataRate(double rate, TimeSpan gracePeriod)
16+
{
17+
if (rate <= 0)
18+
{
19+
throw new ArgumentOutOfRangeException(nameof(rate), CoreStrings.PositiveNumberRequired);
20+
}
21+
22+
if (gracePeriod < TimeSpan.Zero)
23+
{
24+
throw new ArgumentOutOfRangeException(nameof(gracePeriod), CoreStrings.NonNegativeTimeSpanRequired);
25+
}
26+
27+
Rate = rate;
28+
GracePeriod = gracePeriod;
29+
}
30+
31+
/// <summary>
32+
/// The minimum rate in bytes/second at which data should be processed.
33+
/// </summary>
34+
public double Rate { get; }
35+
36+
/// <summary>
37+
/// The amount of time to delay enforcement of <see cref="MinimumDataRate" />.
38+
/// </summary>
39+
public TimeSpan GracePeriod { get; }
40+
}
41+
}

src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs

Lines changed: 117 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,24 @@ public class FrameConnection : IConnectionContext, ITimeoutControl
2929
private long _timeoutTimestamp = long.MaxValue;
3030
private TimeoutAction _timeoutAction;
3131

32+
private object _readTimingLock = new object();
33+
private bool _readTimingEnabled;
34+
private bool _readTimingPauseRequested;
35+
private long _readTimingElapsedTicks;
36+
private long _readTimingBytesRead;
37+
3238
private Task _lifetimeTask;
3339

3440
public FrameConnection(FrameConnectionContext context)
3541
{
3642
_context = context;
3743
}
3844

45+
// For testing
46+
internal Frame Frame => _frame;
47+
48+
public bool TimedOut { get; private set; }
49+
3950
public string ConnectionId => _context.ConnectionId;
4051
public IPipeWriter Input => _context.Input.Writer;
4152
public IPipeReader Output => _context.Output.Reader;
@@ -91,15 +102,7 @@ private async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> app
91102
}
92103

93104
// _frame must be initialized before adding the connection to the connection manager
94-
_frame = new Frame<TContext>(application, new FrameContext
95-
{
96-
ConnectionId = _context.ConnectionId,
97-
ConnectionInformation = _context.ConnectionInformation,
98-
ServiceContext = _context.ServiceContext,
99-
TimeoutControl = this,
100-
Input = input,
101-
Output = output
102-
});
105+
CreateFrame(application, input, output);
103106

104107
// Do this before the first await so we don't yield control to the transport until we've
105108
// added the connection to the connection manager
@@ -140,9 +143,22 @@ private async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> app
140143
}
141144
}
142145

146+
internal void CreateFrame<TContext>(IHttpApplication<TContext> application, IPipeReader input, IPipe output)
147+
{
148+
_frame = new Frame<TContext>(application, new FrameContext
149+
{
150+
ConnectionId = _context.ConnectionId,
151+
ConnectionInformation = _context.ConnectionInformation,
152+
ServiceContext = _context.ServiceContext,
153+
TimeoutControl = this,
154+
Input = input,
155+
Output = output
156+
});
157+
}
158+
143159
public void OnConnectionClosed(Exception ex)
144160
{
145-
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
161+
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
146162

147163
// Abort the connection (if not already aborted)
148164
_frame.Abort(ex);
@@ -152,7 +168,7 @@ public void OnConnectionClosed(Exception ex)
152168

153169
public Task StopAsync()
154170
{
155-
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
171+
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
156172

157173
_frame.Stop();
158174

@@ -161,32 +177,41 @@ public Task StopAsync()
161177

162178
public void Abort(Exception ex)
163179
{
164-
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
180+
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
165181

166182
// Abort the connection (if not already aborted)
167183
_frame.Abort(ex);
168184
}
169185

170186
public Task AbortAsync(Exception ex)
171187
{
172-
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
188+
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
173189

174190
// Abort the connection (if not already aborted)
175191
_frame.Abort(ex);
176192

177193
return _lifetimeTask;
178194
}
179195

180-
public void Timeout()
196+
public void SetTimeoutResponse()
181197
{
182-
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
198+
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
183199

184200
_frame.SetBadRequestState(RequestRejectionReason.RequestTimeout);
185201
}
186202

203+
public void Timeout()
204+
{
205+
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
206+
207+
TimedOut = true;
208+
_readTimingEnabled = false;
209+
_frame.Stop();
210+
}
211+
187212
private async Task<Stream> ApplyConnectionAdaptersAsync()
188213
{
189-
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
214+
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
190215

191216
var features = new FeatureCollection();
192217
var connectionAdapters = _context.ConnectionAdapters;
@@ -231,7 +256,7 @@ private void DisposeAdaptedConnections()
231256

232257
public void Tick(DateTimeOffset now)
233258
{
234-
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
259+
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
235260

236261
var timestamp = now.Ticks;
237262

@@ -242,10 +267,41 @@ public void Tick(DateTimeOffset now)
242267

243268
if (_timeoutAction == TimeoutAction.SendTimeoutResponse)
244269
{
245-
Timeout();
270+
SetTimeoutResponse();
246271
}
247272

248-
_frame.Stop();
273+
Timeout();
274+
}
275+
else
276+
{
277+
lock (_readTimingLock)
278+
{
279+
if (_readTimingEnabled)
280+
{
281+
_readTimingElapsedTicks += timestamp - _lastTimestamp;
282+
283+
if (_frame.RequestBodyMinimumDataRate?.Rate > 0 && _readTimingElapsedTicks > _frame.RequestBodyMinimumDataRate.GracePeriod.Ticks)
284+
{
285+
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
286+
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
287+
288+
if (rate < _frame.RequestBodyMinimumDataRate.Rate)
289+
{
290+
Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, _frame.RequestBodyMinimumDataRate.Rate);
291+
Timeout();
292+
}
293+
}
294+
295+
// PauseTimingReads() cannot just set _timingReads to false. It needs to go through at least one tick
296+
// before pausing, otherwise _readTimingElapsed might never be updated if PauseTimingReads() is always
297+
// called before the next tick.
298+
if (_readTimingPauseRequested)
299+
{
300+
_readTimingEnabled = false;
301+
_readTimingPauseRequested = false;
302+
}
303+
}
304+
}
249305
}
250306

251307
Interlocked.Exchange(ref _lastTimestamp, timestamp);
@@ -275,5 +331,47 @@ private void AssignTimeout(long ticks, TimeoutAction timeoutAction)
275331
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
276332
Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + ticks + Heartbeat.Interval.Ticks);
277333
}
334+
335+
public void StartTimingReads()
336+
{
337+
lock (_readTimingLock)
338+
{
339+
_readTimingElapsedTicks = 0;
340+
_readTimingBytesRead = 0;
341+
_readTimingEnabled = true;
342+
}
343+
}
344+
345+
public void StopTimingReads()
346+
{
347+
lock (_readTimingLock)
348+
{
349+
_readTimingEnabled = false;
350+
}
351+
}
352+
353+
public void PauseTimingReads()
354+
{
355+
lock (_readTimingLock)
356+
{
357+
_readTimingPauseRequested = true;
358+
}
359+
}
360+
361+
public void ResumeTimingReads()
362+
{
363+
lock (_readTimingLock)
364+
{
365+
_readTimingEnabled = true;
366+
367+
// In case pause and resume were both called between ticks
368+
_readTimingPauseRequested = false;
369+
}
370+
}
371+
372+
public void BytesRead(int count)
373+
{
374+
Interlocked.Add(ref _readTimingBytesRead, count);
375+
}
278376
}
279377
}

0 commit comments

Comments
 (0)