Skip to content

Commit 725fa34

Browse files
authored
Deflake Kestrel low response rate tests (#13532)
1 parent 806fef3 commit 725fa34

File tree

1 file changed

+133
-14
lines changed

1 file changed

+133
-14
lines changed

src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs

Lines changed: 133 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Microsoft.AspNetCore.Http;
1818
using Microsoft.AspNetCore.Http.Features;
1919
using Microsoft.AspNetCore.Server.Kestrel.Core;
20+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
2021
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
2122
using Microsoft.AspNetCore.Server.Kestrel.Https;
2223
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
@@ -756,6 +757,9 @@ public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLarg
756757
};
757758

758759
testContext.InitializeHeartbeat();
760+
var dateHeaderValueManager = new DateHeaderValueManager();
761+
dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue);
762+
testContext.DateHeaderValueManager = dateHeaderValueManager;
759763

760764
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
761765

@@ -782,16 +786,23 @@ async Task App(HttpContext context)
782786
await connection.Send(
783787
"GET / HTTP/1.1",
784788
"Host:",
785-
"Connection: close",
786789
"",
787790
"");
788791

789-
var minTotalOutputSize = chunkCount * chunkSize;
792+
await connection.Receive(
793+
"HTTP/1.1 200 OK",
794+
$"Date: {dateHeaderValueManager.GetDateHeaderValues().String}");
790795

791796
// Make sure consuming a single chunk exceeds the 2 second timeout.
792797
var targetBytesPerSecond = chunkSize / 4;
793-
await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond);
798+
799+
// expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
800+
// the response header writing logic or response body chunking logic itself changes.
801+
await AssertBytesReceivedAtTargetRate(connection.Stream, expectedBytes: 33_553_537, targetBytesPerSecond);
794802
await appFuncCompleted.Task.DefaultTimeout();
803+
804+
connection.ShutdownSend();
805+
await connection.WaitForConnectionClose();
795806
}
796807
await server.StopAsync();
797808
}
@@ -801,11 +812,7 @@ await connection.Send(
801812
Assert.False(requestAborted);
802813
}
803814

804-
private bool ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeadersRetryPredicate(Exception e)
805-
=> e is IOException && e.Message.Contains("Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request");
806-
807815
[Fact]
808-
[Flaky("https://github.com/dotnet/corefx/issues/30691", FlakyOn.AzP.Windows)]
809816
[CollectDump]
810817
public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders()
811818
{
@@ -830,6 +837,9 @@ public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLarg
830837
};
831838

832839
testContext.InitializeHeartbeat();
840+
var dateHeaderValueManager = new DateHeaderValueManager();
841+
dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue);
842+
testContext.DateHeaderValueManager = dateHeaderValueManager;
833843

834844
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
835845

@@ -859,6 +869,86 @@ await connection.Send(
859869
"");
860870
}
861871

872+
await connection.Send(
873+
"GET / HTTP/1.1",
874+
"Host:",
875+
"",
876+
"");
877+
878+
await connection.Receive(
879+
"HTTP/1.1 200 OK",
880+
$"Date: {dateHeaderValueManager.GetDateHeaderValues().String}");
881+
882+
var minResponseSize = headerSize * headerCount;
883+
var minTotalOutputSize = requestCount * minResponseSize;
884+
885+
// Make sure consuming a single set of response headers exceeds the 2 second timeout.
886+
var targetBytesPerSecond = minResponseSize / 4;
887+
888+
// expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
889+
// the response header writing logic itself changes.
890+
await AssertBytesReceivedAtTargetRate(connection.Stream, expectedBytes: 268_439_596, targetBytesPerSecond);
891+
connection.ShutdownSend();
892+
await connection.WaitForConnectionClose();
893+
}
894+
895+
await server.StopAsync();
896+
}
897+
898+
mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>()), Times.Never());
899+
mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny<string>()), Times.Once());
900+
Assert.False(requestAborted);
901+
}
902+
903+
[Fact]
904+
[Flaky("https://github.com/aspnet/AspNetCore/issues/13219", FlakyOn.AzP.Linux, FlakyOn.Helix.All)]
905+
public async Task ClientCanReceiveFullConnectionCloseResponseWithoutErrorAtALowDataRate()
906+
{
907+
var chunkSize = 64 * 128 * 1024;
908+
var chunkCount = 4;
909+
var chunkData = new byte[chunkSize];
910+
911+
var requestAborted = false;
912+
var appFuncCompleted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
913+
var mockKestrelTrace = new Mock<IKestrelTrace>();
914+
915+
var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)
916+
{
917+
ServerOptions =
918+
{
919+
Limits =
920+
{
921+
MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2))
922+
}
923+
}
924+
};
925+
926+
testContext.InitializeHeartbeat();
927+
var dateHeaderValueManager = new DateHeaderValueManager();
928+
dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue);
929+
testContext.DateHeaderValueManager = dateHeaderValueManager;
930+
931+
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
932+
933+
async Task App(HttpContext context)
934+
{
935+
context.RequestAborted.Register(() =>
936+
{
937+
requestAborted = true;
938+
});
939+
940+
for (var i = 0; i < chunkCount; i++)
941+
{
942+
await context.Response.BodyWriter.WriteAsync(new Memory<byte>(chunkData, 0, chunkData.Length), context.RequestAborted);
943+
}
944+
945+
appFuncCompleted.SetResult(null);
946+
}
947+
948+
using (var server = new TestServer(App, testContext, listenOptions))
949+
{
950+
using (var connection = server.CreateConnection())
951+
{
862952
// Close the connection with the last request so AssertStreamCompleted actually completes.
863953
await connection.Send(
864954
"GET / HTTP/1.1",
@@ -867,12 +957,18 @@ await connection.Send(
867957
"",
868958
"");
869959

870-
var responseSize = headerSize * headerCount;
871-
var minTotalOutputSize = requestCount * responseSize;
960+
await connection.Receive(
961+
"HTTP/1.1 200 OK",
962+
"Connection: close",
963+
$"Date: {dateHeaderValueManager.GetDateHeaderValues().String}");
964+
965+
// Make sure consuming a single chunk exceeds the 2 second timeout.
966+
var targetBytesPerSecond = chunkSize / 4;
872967

873-
// Make sure consuming a single set of response headers exceeds the 2 second timeout.
874-
var targetBytesPerSecond = responseSize / 4;
875-
await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond);
968+
// expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
969+
// the response header writing logic or response body chunking logic itself changes.
970+
await AssertStreamCompletedAtTargetRate(connection.Stream, expectedBytes: 33_553_556, targetBytesPerSecond);
971+
await appFuncCompleted.Task.DefaultTimeout();
876972
}
877973
await server.StopAsync();
878974
}
@@ -909,7 +1005,30 @@ private async Task AssertStreamAborted(Stream stream, int totalBytes)
9091005
Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully.");
9101006
}
9111007

912-
private async Task AssertStreamCompleted(Stream stream, long minimumBytes, int targetBytesPerSecond)
1008+
private async Task AssertBytesReceivedAtTargetRate(Stream stream, int expectedBytes, int targetBytesPerSecond)
1009+
{
1010+
var receiveBuffer = new byte[64 * 1024];
1011+
var totalReceived = 0;
1012+
var startTime = DateTimeOffset.UtcNow;
1013+
1014+
do
1015+
{
1016+
var received = await stream.ReadAsync(receiveBuffer, 0, Math.Min(receiveBuffer.Length, expectedBytes - totalReceived));
1017+
1018+
Assert.NotEqual(0, received);
1019+
1020+
totalReceived += received;
1021+
1022+
var expectedTimeElapsed = TimeSpan.FromSeconds(totalReceived / targetBytesPerSecond);
1023+
var timeElapsed = DateTimeOffset.UtcNow - startTime;
1024+
if (timeElapsed < expectedTimeElapsed)
1025+
{
1026+
await Task.Delay(expectedTimeElapsed - timeElapsed);
1027+
}
1028+
} while (totalReceived < expectedBytes);
1029+
}
1030+
1031+
private async Task AssertStreamCompletedAtTargetRate(Stream stream, long expectedBytes, int targetBytesPerSecond)
9131032
{
9141033
var receiveBuffer = new byte[64 * 1024];
9151034
var received = 0;
@@ -929,7 +1048,7 @@ private async Task AssertStreamCompleted(Stream stream, long minimumBytes, int t
9291048
}
9301049
} while (received > 0);
9311050

932-
Assert.True(totalReceived >= minimumBytes, $"{nameof(AssertStreamCompleted)} Stream aborted prematurely.");
1051+
Assert.Equal(expectedBytes, totalReceived);
9331052
}
9341053

9351054
public static TheoryData<string, StringValues, string> NullHeaderData

0 commit comments

Comments
 (0)