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

Commit b080a94

Browse files
authored
Fix rate limiting for repeated small writes (#8)
1 parent 5532528 commit b080a94

File tree

3 files changed

+70
-10
lines changed

3 files changed

+70
-10
lines changed

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -435,18 +435,27 @@ public void StartTimingWrite(long size)
435435

436436
if (minResponseDataRate != null)
437437
{
438-
var timeoutTicks = Math.Max(
439-
minResponseDataRate.GracePeriod.Ticks,
440-
TimeSpan.FromSeconds(size / minResponseDataRate.BytesPerSecond).Ticks);
441-
442438
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
439+
var currentTimeUpperBound = _lastTimestamp + Heartbeat.Interval.Ticks;
440+
var ticksToCompleteWriteAtMinRate = TimeSpan.FromSeconds(size / minResponseDataRate.BytesPerSecond).Ticks;
443441

444-
// Don't penalize a connection for completing previous flushes more quickly than required.
442+
// If ticksToCompleteWriteAtMinRate is less than the configured grace period,
443+
// allow that write to take up to the grace period to complete. Only add the grace period
444+
// to the current time and not to any accumulated timeout.
445+
var singleWriteTimeoutTimestamp = currentTimeUpperBound + Math.Max(
446+
minResponseDataRate.GracePeriod.Ticks,
447+
ticksToCompleteWriteAtMinRate);
448+
449+
// Don't penalize a connection for completing previous writes more quickly than required.
445450
// We don't want to kill a connection when flushing the chunk terminator just because the previous
446451
// chunk was large if the previous chunk was flushed quickly.
447-
_writeTimingTimeoutTimestamp = Math.Max(_writeTimingTimeoutTimestamp, _lastTimestamp + Heartbeat.Interval.Ticks);
448452

449-
_writeTimingTimeoutTimestamp += timeoutTicks;
453+
// Don't add any grace period to this accumulated timeout because the grace period could
454+
// get accumulated repeatedly making the timeout for a bunch of consecutive small writes
455+
// far too conservative.
456+
var accumulatedWriteTimeoutTimestamp = _writeTimingTimeoutTimestamp + ticksToCompleteWriteAtMinRate;
457+
458+
_writeTimingTimeoutTimestamp = Math.Max(singleWriteTimeoutTimestamp, accumulatedWriteTimeoutTimestamp);
450459
_writeTimingWrites++;
451460
}
452461
}

test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,5 +529,56 @@ public void WriteTimingTimeoutPushedOnConcurrentWrite()
529529
Assert.True(_frameConnection.TimedOut);
530530
Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
531531
}
532+
533+
[Fact]
534+
public async Task WriteTimingAbortsConnectionWhenRepeadtedSmallWritesDoNotCompleteWithMinimumDataRate()
535+
{
536+
var systemClock = new MockSystemClock();
537+
var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5));
538+
var numWrites = 5;
539+
var writeSize = 100;
540+
var aborted = new TaskCompletionSource<object>();
541+
542+
_frameConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate;
543+
_frameConnectionContext.ServiceContext.SystemClock = systemClock;
544+
545+
var mockLogger = new Mock<IKestrelTrace>();
546+
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
547+
548+
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
549+
_frameConnection.Frame.Reset();
550+
_frameConnection.Frame.RequestAborted.Register(() =>
551+
{
552+
aborted.SetResult(null);
553+
});
554+
555+
// Initialize timestamp
556+
var startTime = systemClock.UtcNow;
557+
_frameConnection.Tick(startTime);
558+
559+
// 5 consecutive 100 byte writes.
560+
for (var i = 0; i < numWrites - 1; i++)
561+
{
562+
_frameConnection.StartTimingWrite(writeSize);
563+
_frameConnection.StopTimingWrite();
564+
}
565+
566+
// Stall the last write.
567+
_frameConnection.StartTimingWrite(writeSize);
568+
569+
// Move the clock forward Heartbeat.Interval + MinDataRate.GracePeriod + 4 seconds.
570+
// The grace period should only be added for the first write. The subsequent 4 100 byte writes should add 1 second each to the timeout given the 100 byte/s min rate.
571+
systemClock.UtcNow += Heartbeat.Interval + minResponseDataRate.GracePeriod + TimeSpan.FromSeconds((numWrites - 1) * writeSize / minResponseDataRate.BytesPerSecond);
572+
_frameConnection.Tick(systemClock.UtcNow);
573+
574+
Assert.False(_frameConnection.TimedOut);
575+
576+
// On more tick forward triggers the timeout.
577+
systemClock.UtcNow += TimeSpan.FromTicks(1);
578+
_frameConnection.Tick(systemClock.UtcNow);
579+
580+
Assert.True(_frameConnection.TimedOut);
581+
await aborted.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
582+
}
532583
}
533584
}

test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2446,8 +2446,8 @@ await connection.Receive(
24462446
public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate()
24472447
{
24482448
// Smaller chunks allow for quicker response rate timeouts.
2449-
var chunkSize = 1024;
2450-
var chunks = 8192;
2449+
const int chunkSize = 1024;
2450+
const int chunks = 256 * 1024;;
24512451

24522452
var responseRateTimeoutMessageLogged = new TaskCompletionSource<object>();
24532453
var connectionStopMessageLogged = new TaskCompletionSource<object>();
@@ -2518,7 +2518,7 @@ await connection.Receive(
25182518
public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate()
25192519
{
25202520
const int chunkSize = 1024;
2521-
const int chunks = 8192;
2521+
const int chunks = 256 * 1024;
25222522

25232523
var certificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
25242524

0 commit comments

Comments
 (0)