Always flush HTTP/2 headers despite flow control (#20831)

This commit is contained in:
Stephen Halter 2020-04-15 09:22:27 -07:00 committed by GitHub
parent 162817d3f0
commit ea555458dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 57 additions and 30 deletions

View File

@ -254,7 +254,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
public ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence<byte> data, bool endStream, bool forceFlush)
public ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence<byte> data, bool endStream, bool firstWrite, bool forceFlush)
{
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
var dataLength = data.Length;
@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.1
if (dataLength != 0 && dataLength > flowControl.Available)
{
return WriteDataAsync(streamId, flowControl, data, dataLength, endStream);
return WriteDataAsync(streamId, flowControl, data, dataLength, endStream, firstWrite);
}
// This cast is safe since if dataLength would overflow an int, it's guaranteed to be greater than the available flow control window.
@ -370,7 +370,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
private async ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, long dataLength, bool endStream)
private async ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, long dataLength, bool endStream, bool firstWrite)
{
FlushResult flushResult = default;
@ -406,6 +406,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// flow control induced backpressure below.
writeTask = _flusher.FlushAsync();
}
else if (firstWrite)
{
// If we're facing flow control induced backpressure on the first write for a given stream's response body,
// we make sure to flush the response headers immediately.
writeTask = _flusher.FlushAsync();
}
firstWrite = false;
if (_minResponseDataRate != null)
{

View File

@ -405,6 +405,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
do
{
var firstWrite = true;
readResult = await _pipeReader.ReadAsync();
if (readResult.IsCanceled)
@ -420,7 +422,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
// Only flush if required (i.e. content length exceeds flow control availability)
// Writing remaining content without flushing allows content and trailers to be sent in the same packet
await _frameWriter.WriteDataAsync(StreamId, _flowControl, readResult.Buffer, endStream: false, forceFlush: false);
await _frameWriter.WriteDataAsync(StreamId, _flowControl, readResult.Buffer, endStream: false, firstWrite, forceFlush: false);
}
_stream.ResponseTrailers.SetReadOnly();
@ -440,13 +442,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
else
{
var endStream = readResult.IsCompleted;
if (endStream)
{
_stream.DecrementActiveClientStreamCount();
}
flushResult = await _frameWriter.WriteDataAsync(StreamId, _flowControl, readResult.Buffer, endStream, forceFlush: true);
flushResult = await _frameWriter.WriteDataAsync(StreamId, _flowControl, readResult.Buffer, endStream, firstWrite, forceFlush: true);
}
firstWrite = false;
_pipeReader.AdvanceTo(readResult.Buffer.End);
} while (!readResult.IsCompleted);
}

View File

@ -28,6 +28,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class Http2ConnectionTests : Http2TestBase
{
[Fact]
public async Task FlowControl_NoAvailability_ResponseHeadersStillFlushed()
{
_clientSettings.InitialWindowSize = 0;
await InitializeConnectionAsync(c =>
{
return c.Response.Body.WriteAsync(new byte[1]).AsTask();
});
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await SendWindowUpdateAsync(streamId: 1, 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 1,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
[Fact]
[QuarantinedTest]
public async Task FlowControl_ParallelStreams_FirstInFirstOutOrder()
@ -38,34 +70,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// 3. Update the connection window one byte at a time.
// 4. Read from them in a FIFO order until they are each complete.
var writeTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
// Ensure there that all backpressure is due to connection flow control, not the pipe.
_serviceContext.ServerOptions.Limits.MaxResponseBufferSize = long.MaxValue;
// Ensure there that all backpressure is due to connection flow control, not stream flow control.
_clientSettings.InitialWindowSize = int.MaxValue;
await InitializeConnectionAsync(async c =>
await InitializeConnectionAsync(c =>
{
// Send headers
await c.Response.Body.FlushAsync();
var responseBodySize = Convert.ToInt32(c.Request.Headers["ResponseBodySize"]);
var writeTask = c.Response.Body.WriteAsync(new byte[responseBodySize]);
// Notify test that write has started
writeTcs.SetResult(null);
// Wait for write to complete
await writeTask;
return c.Response.Body.WriteAsync(new byte[responseBodySize]).AsTask();
});
// Consume the entire connection output flow control window with a large response.
await StartStreamAsync(1, GetHeaders(responseBodySize: 65535), endStream: true);
// Ensure the stream window size is large enough
await SendWindowUpdateAsync(streamId: 1, 65535);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await writeTcs.Task;
await ExpectAsync(Http2FrameType.DATA,
withLength: 16384,
withFlags: (byte)Http2DataFrameFlags.NONE,
@ -87,8 +109,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
writeTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
await StartStreamAsync(3, GetHeaders(responseBodySize: 3), endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
@ -96,10 +116,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await writeTcs.Task;
writeTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
await StartStreamAsync(5, GetHeaders(responseBodySize: 3), endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
@ -107,8 +123,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 5);
await writeTcs.Task;
await SendWindowUpdateAsync(streamId: 0, 1);
// FIFO means stream 3 returns data first