Skip to content

Commit 17d072f

Browse files
authored
Send window updates based on examined rather than consumed. (#8200)
1 parent 16cd69f commit 17d072f

File tree

2 files changed

+142
-3
lines changed

2 files changed

+142
-3
lines changed

src/Servers/Kestrel/Core/src/Internal/Http2/Http2MessageBody.cs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class Http2MessageBody : MessageBody
1414
{
1515
private readonly Http2Stream _context;
1616
private ReadResult _readResult;
17+
private long _alreadyExaminedInNextReadResult;
1718

1819
private Http2MessageBody(Http2Stream context, MinDataRate minRequestBodyDataRate)
1920
: base(context, minRequestBodyDataRate)
@@ -63,14 +64,57 @@ public override void AdvanceTo(SequencePosition consumed)
6364

6465
public override void AdvanceTo(SequencePosition consumed, SequencePosition examined)
6566
{
66-
var dataLength = _readResult.Buffer.Slice(_readResult.Buffer.Start, consumed).Length;
67+
// This code path is fairly hard to understand so let's break it down with an example
68+
// ReadAsync returns a ReadResult of length 50.
69+
// Advance(25, 40). The examined length would be 40 and consumed length would be 25.
70+
// _totalExaminedInPreviousReadResult starts at 0. newlyExamined is 40.
71+
// OnDataRead is called with length 40.
72+
// _totalExaminedInPreviousReadResult is now 40 - 25 = 15.
73+
74+
// The next call to ReadAsync returns 50 again
75+
// Advance(5, 5) is called
76+
// newlyExamined is 5 - 15, or -10.
77+
// Update _totalExaminedInPreviousReadResult to 10 as we consumed 5.
78+
79+
// The next call to ReadAsync returns 50 again
80+
// _totalExaminedInPreviousReadResult is 10
81+
// Advance(50, 50) is called
82+
// newlyExamined = 50 - 10 = 40
83+
// _totalExaminedInPreviousReadResult is now 50
84+
// _totalExaminedInPreviousReadResult is finally 0 after subtracting consumedLength.
85+
86+
long examinedLength;
87+
long consumedLength;
88+
if (consumed.Equals(examined))
89+
{
90+
examinedLength = _readResult.Buffer.Slice(_readResult.Buffer.Start, examined).Length;
91+
consumedLength = examinedLength;
92+
}
93+
else
94+
{
95+
consumedLength = _readResult.Buffer.Slice(_readResult.Buffer.Start, consumed).Length;
96+
examinedLength = consumedLength + _readResult.Buffer.Slice(consumed, examined).Length;
97+
}
98+
6799
_context.RequestBodyPipe.Reader.AdvanceTo(consumed, examined);
68-
OnDataRead(dataLength);
100+
101+
var newlyExamined = examinedLength - _alreadyExaminedInNextReadResult;
102+
103+
if (newlyExamined > 0)
104+
{
105+
OnDataRead(newlyExamined);
106+
_alreadyExaminedInNextReadResult += newlyExamined;
107+
}
108+
109+
_alreadyExaminedInNextReadResult -= consumedLength;
69110
}
70111

71112
public override bool TryRead(out ReadResult readResult)
72113
{
73-
return _context.RequestBodyPipe.Reader.TryRead(out readResult);
114+
var result = _context.RequestBodyPipe.Reader.TryRead(out readResult);
115+
_readResult = readResult;
116+
117+
return result;
74118
}
75119

76120
public override async ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading;
1111
using System.Threading.Tasks;
1212
using Microsoft.AspNetCore.Connections;
13+
using Microsoft.AspNetCore.Http;
1314
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
1415
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
1516
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
@@ -682,6 +683,100 @@ await ExpectAsync(Http2FrameType.DATA,
682683
Assert.Equal(updateSize, connectionWindowUpdateFrame.WindowUpdateSizeIncrement);
683684
}
684685

686+
[Fact]
687+
public async Task DATA_BufferRequestBodyLargerThanStreamSizeSmallerThanConnectionPipe_Works()
688+
{
689+
var initialStreamWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
690+
var framesInStreamWindow = initialStreamWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
691+
var initialConnectionWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
692+
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
693+
694+
// Grow the client stream windows so no stream WINDOW_UPDATEs need to be sent.
695+
_clientSettings.InitialWindowSize = int.MaxValue;
696+
697+
await InitializeConnectionAsync(async context =>
698+
{
699+
await context.Response.BodyWriter.FlushAsync();
700+
var readResult = await context.Request.BodyReader.ReadAsync();
701+
while (readResult.Buffer.Length != _maxData.Length * 4)
702+
{
703+
context.Request.BodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
704+
readResult = await context.Request.BodyReader.ReadAsync();
705+
}
706+
707+
context.Request.BodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
708+
709+
readResult = await context.Request.BodyReader.ReadAsync();
710+
Assert.Equal(readResult.Buffer.Length, _maxData.Length * 5);
711+
712+
await context.Response.BodyWriter.WriteAsync(readResult.Buffer.ToArray());
713+
714+
context.Request.BodyReader.AdvanceTo(readResult.Buffer.End);
715+
});
716+
717+
// Grow the client connection windows so no connection WINDOW_UPDATEs need to be sent.
718+
await SendWindowUpdateAsync(0, int.MaxValue - (int)Http2PeerSettings.DefaultInitialWindowSize);
719+
720+
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
721+
722+
// Rounds down so we don't go over the half window size and trigger an update
723+
for (var i = 0; i < framesInStreamWindow / 2; i++)
724+
{
725+
await SendDataAsync(1, _maxData, endStream: false);
726+
}
727+
728+
// trip over the update size.
729+
await SendDataAsync(1, _maxData, endStream: false);
730+
731+
await ExpectAsync(Http2FrameType.HEADERS,
732+
withLength: 37,
733+
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
734+
withStreamId: 1);
735+
736+
var dataFrames = new List<Http2FrameWithPayload>();
737+
738+
var streamWindowUpdateFrame1 = await ExpectAsync(Http2FrameType.WINDOW_UPDATE,
739+
withLength: 4,
740+
withFlags: (byte)Http2DataFrameFlags.NONE,
741+
withStreamId: 1);
742+
743+
// Writing over half the initial window size induces both a connection-level and stream-level window update.
744+
745+
await SendDataAsync(1, _maxData, endStream: true);
746+
747+
for (var i = 0; i < framesInStreamWindow / 2 + 2; i++)
748+
{
749+
var dataFrame3 = await ExpectAsync(Http2FrameType.DATA,
750+
withLength: _maxData.Length,
751+
withFlags: (byte)Http2DataFrameFlags.NONE,
752+
withStreamId: 1);
753+
dataFrames.Add(dataFrame3);
754+
}
755+
756+
var connectionWindowUpdateFrame = await ExpectAsync(Http2FrameType.WINDOW_UPDATE,
757+
withLength: 4,
758+
withFlags: (byte)Http2DataFrameFlags.NONE,
759+
withStreamId: 0);
760+
// End
761+
762+
await ExpectAsync(Http2FrameType.DATA,
763+
withLength: 0,
764+
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
765+
withStreamId: 1);
766+
767+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
768+
769+
foreach (var frame in dataFrames)
770+
{
771+
Assert.True(_maxData.AsSpan().SequenceEqual(frame.PayloadSequence.ToArray()));
772+
}
773+
774+
var updateSize = ((framesInStreamWindow / 2) + 1) * _maxData.Length;
775+
Assert.Equal(updateSize, streamWindowUpdateFrame1.WindowUpdateSizeIncrement);
776+
updateSize = ((framesInConnectionWindow / 2) + 1) * _maxData.Length;
777+
Assert.Equal(updateSize, connectionWindowUpdateFrame.WindowUpdateSizeIncrement);
778+
}
779+
685780
[Fact]
686781
public async Task DATA_Received_StreamIdZero_ConnectionError()
687782
{

0 commit comments

Comments
 (0)