diff --git a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs index 405b339436..fe97317ff0 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs @@ -225,7 +225,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 ( pool: pool, readerScheduler: PipeScheduler.Inline, - writerScheduler: PipeScheduler.Inline, + writerScheduler: PipeScheduler.ThreadPool, pauseWriterThreshold: 1, resumeWriterThreshold: 1, useSynchronizationContext: false, diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 7df7750f81..8c623167fb 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.ExceptionServices; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -1687,5 +1688,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.IsType(thrownEx.InnerException); Assert.Equal(CoreStrings.ConnectionAbortedByApplication, thrownEx.InnerException.Message); } + + // Sync writes after async writes could block the write loop if the callback is not dispatched. + // https://github.com/aspnet/KestrelHttpServer/issues/2878 + [Fact] + public async Task Write_DoesNotBlockWriteLoop() + { + const int windowSize = (int)Http2PeerSettings.DefaultMaxFrameSize; + _clientSettings.InitialWindowSize = windowSize; + + await InitializeConnectionAsync(async context => + { + // Fill the flow control window to create async back pressure. + await context.Response.Body.WriteAsync(new byte[windowSize + 1], 0, windowSize + 1); + context.Response.Body.Write(new byte[1], 0, 1); + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + + await ExpectAsync(Http2FrameType.DATA, + withLength: windowSize, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + await SendWindowUpdateAsync(1, 2); + await SendWindowUpdateAsync(0, 2); + + // Remaining 1 byte from the first write and then the second write + await ExpectAsync(Http2FrameType.DATA, + withLength: 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 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); + } } } \ No newline at end of file