Use the default pipe options for backpressure (#21001)

* Use the default pipe options for backpressure
- This controls memory so clients aren't easily overwhelmed. With the changes made to Pipe to no longer throw if the pause threshold is exceeded makes this work well.
- Remove PipeReaderFactory
- Implement cancellation in SSE

Contributes to #17797
This commit is contained in:
David Fowler 2020-04-21 16:31:18 -07:00 committed by GitHub
parent e89e8d1578
commit f9a9788c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 81 deletions

View File

@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
.Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>())) .Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
.Returns(copyToAsyncTcs.Task); .Returns(copyToAsyncTcs.Task);
mockStream.Setup(s => s.CanRead).Returns(true); mockStream.Setup(s => s.CanRead).Returns(true);
return new HttpResponseMessage {Content = new StreamContent(mockStream.Object)}; return new HttpResponseMessage { Content = new StreamContent(mockStream.Object) };
}); });
try try
@ -76,15 +76,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{ {
var mockStream = new Mock<Stream>(); var mockStream = new Mock<Stream>();
mockStream mockStream
.Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>())) .Setup(s => s.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()))
.Returns<Stream, int, CancellationToken>(async (stream, bufferSize, t) => .Returns<Memory<byte>, CancellationToken>(async (data, t) =>
{ {
var buffer = Encoding.ASCII.GetBytes("data: 3:abc\r\n\r\n"); if (t.IsCancellationRequested)
while (!t.IsCancellationRequested)
{ {
await stream.WriteAsync(buffer, 0, buffer.Length).OrTimeout(); return 0;
await Task.Delay(100);
} }
int count = Encoding.ASCII.GetBytes("data: 3:abc\r\n\r\n", data.Span);
await Task.Delay(100);
return count;
}); });
mockStream.Setup(s => s.CanRead).Returns(true); mockStream.Setup(s => s.CanRead).Returns(true);
@ -120,6 +122,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public async Task SSETransportStopsWithErrorIfServerSendsIncompleteResults() public async Task SSETransportStopsWithErrorIfServerSendsIncompleteResults()
{ {
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
var calls = 0;
mockHttpHandler.Protected() mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) => .Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
@ -128,11 +131,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var mockStream = new Mock<Stream>(); var mockStream = new Mock<Stream>();
mockStream mockStream
.Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>())) .Setup(s => s.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()))
.Returns<Stream, int, CancellationToken>(async (stream, bufferSize, t) => .Returns<Memory<byte>, CancellationToken>((data, t) =>
{ {
var buffer = Encoding.ASCII.GetBytes("data: 3:a"); if (calls == 0)
await stream.WriteAsync(buffer, 0, buffer.Length); {
calls++;
return new ValueTask<int>(Encoding.ASCII.GetBytes("data: 3:a", data.Span));
}
return new ValueTask<int>(0);
}); });
mockStream.Setup(s => s.CanRead).Returns(true); mockStream.Setup(s => s.CanRead).Returns(true);
@ -165,7 +172,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
} }
var eventStreamTcs = new TaskCompletionSource<object>(); var eventStreamTcs = new TaskCompletionSource<object>();
var copyToAsyncTcs = new TaskCompletionSource<int>(); var readTcs = new TaskCompletionSource<int>();
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -182,8 +189,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
// returns unfinished task to block pipelines // returns unfinished task to block pipelines
var mockStream = new Mock<Stream>(); var mockStream = new Mock<Stream>();
mockStream mockStream
.Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>())) .Setup(s => s.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()))
.Returns(copyToAsyncTcs.Task); .Returns<Memory<byte>, CancellationToken>(async (data, ct) =>
{
using (ct.Register(() => readTcs.TrySetCanceled()))
{
return await readTcs.Task;
}
});
mockStream.Setup(s => s.CanRead).Returns(true); mockStream.Setup(s => s.CanRead).Returns(true);
return new HttpResponseMessage { Content = new StreamContent(mockStream.Object) }; return new HttpResponseMessage { Content = new StreamContent(mockStream.Object) };
} }
@ -214,7 +227,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public async Task SSETransportStopsIfChannelClosed() public async Task SSETransportStopsIfChannelClosed()
{ {
var eventStreamTcs = new TaskCompletionSource<object>(); var eventStreamTcs = new TaskCompletionSource<object>();
var copyToAsyncTcs = new TaskCompletionSource<int>(); var readTcs = new TaskCompletionSource<int>();
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -229,8 +242,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
// returns unfinished task to block pipelines // returns unfinished task to block pipelines
var mockStream = new Mock<Stream>(); var mockStream = new Mock<Stream>();
mockStream mockStream
.Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>())) .Setup(s => s.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()))
.Returns(copyToAsyncTcs.Task); .Returns<Memory<byte>, CancellationToken>(async (data, ct) =>
{
using (ct.Register(() => readTcs.TrySetCanceled()))
{
return await readTcs.Task;
}
});
mockStream.Setup(s => s.CanRead).Returns(true); mockStream.Setup(s => s.CanRead).Returns(true);
return new HttpResponseMessage { Content = new StreamContent(mockStream.Object) }; return new HttpResponseMessage { Content = new StreamContent(mockStream.Object) };
}); });
@ -281,7 +300,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public async Task SSETransportCancelsSendOnStop() public async Task SSETransportCancelsSendOnStop()
{ {
var eventStreamTcs = new TaskCompletionSource<object>(); var eventStreamTcs = new TaskCompletionSource<object>();
var copyToAsyncTcs = new TaskCompletionSource<object>(); var readTcs = new TaskCompletionSource<object>();
var sendSyncPoint = new SyncPoint(); var sendSyncPoint = new SyncPoint();
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
@ -299,10 +318,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
// returns unfinished task to block pipelines // returns unfinished task to block pipelines
var mockStream = new Mock<Stream>(); var mockStream = new Mock<Stream>();
mockStream mockStream
.Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>())) .Setup(s => s.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()))
.Returns<Stream, int, CancellationToken>(async (stream, bufferSize, t) => .Returns(async () =>
{ {
await copyToAsyncTcs.Task; await readTcs.Task;
throw new TaskCanceledException(); throw new TaskCanceledException();
}); });
@ -332,7 +351,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var stopTask = sseTransport.StopAsync(); var stopTask = sseTransport.StopAsync();
copyToAsyncTcs.SetResult(null); readTcs.SetResult(null);
sendSyncPoint.Continue(); sendSyncPoint.Continue();
await stopTask; await stopTask;

View File

@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
{ {
internal static class ClientPipeOptions internal static class ClientPipeOptions
{ {
public static PipeOptions DefaultOptions = new PipeOptions(writerScheduler: PipeScheduler.ThreadPool, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false, pauseWriterThreshold: 0, resumeWriterThreshold: 0); public static PipeOptions DefaultOptions = new PipeOptions(writerScheduler: PipeScheduler.ThreadPool, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
} }
} }

View File

@ -1,48 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Threading;
using System.Threading.Tasks;
namespace System.IO.Pipelines
{
internal class PipeReaderFactory
{
private static readonly Action<object> _cancelReader = state => ((PipeReader)state).CancelPendingRead();
public static PipeReader CreateFromStream(PipeOptions options, Stream stream, CancellationToken cancellationToken)
{
if (!stream.CanRead)
{
throw new NotSupportedException();
}
var pipe = new Pipe(options);
_ = CopyToAsync(stream, pipe, cancellationToken);
return pipe.Reader;
}
private static async Task CopyToAsync(Stream stream, Pipe pipe, CancellationToken cancellationToken)
{
// We manually register for cancellation here in case the Stream implementation ignores it
using (var registration = cancellationToken.Register(_cancelReader, pipe.Reader))
{
try
{
await stream.CopyToAsync(new PipeWriterStream(pipe.Writer), bufferSize: 4096, cancellationToken);
}
catch (OperationCanceledException)
{
// Ignore the cancellation signal (the pipe reader is already wired up for cancellation when the token trips)
}
catch (Exception ex)
{
pipe.Writer.Complete(ex);
return;
}
pipe.Writer.Complete();
}
}
}
}

View File

@ -129,12 +129,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
private async Task ProcessEventStream(HttpResponseMessage response, CancellationToken cancellationToken) private async Task ProcessEventStream(HttpResponseMessage response, CancellationToken cancellationToken)
{ {
Log.StartReceive(_logger); Log.StartReceive(_logger);
static void CancelReader(object state) => ((PipeReader)state).CancelPendingRead();
using (response) using (response)
using (var stream = await response.Content.ReadAsStreamAsync()) using (var stream = await response.Content.ReadAsStreamAsync())
{ {
var options = new PipeOptions(pauseWriterThreshold: 0, resumeWriterThreshold: 0); var reader = PipeReader.Create(stream);
var reader = PipeReaderFactory.CreateFromStream(options, stream, cancellationToken);
using var registration = cancellationToken.Register(CancelReader, reader);
try try
{ {

View File

@ -329,15 +329,30 @@ namespace Microsoft.AspNetCore.SignalR.Tests
logger.LogInformation("Started connection to {url}", url); logger.LogInformation("Started connection to {url}", url);
var bytes = Encoding.UTF8.GetBytes(message); var bytes = Encoding.UTF8.GetBytes(message);
logger.LogInformation("Sending {length} byte message", bytes.Length);
await connection.Transport.Output.WriteAsync(bytes).OrTimeout();
logger.LogInformation("Sent message");
logger.LogInformation("Receiving message"); async Task SendMessage()
// Big timeout here because it can take a while to receive all the bytes {
var receivedData = await connection.Transport.Input.ReadAsync(bytes.Length).OrTimeout(TimeSpan.FromMinutes(2)); logger.LogInformation("Sending {length} byte message", bytes.Length);
Assert.Equal(message, Encoding.UTF8.GetString(receivedData)); await connection.Transport.Output.WriteAsync(bytes).OrTimeout();
logger.LogInformation("Completed receive"); logger.LogInformation("Sent message");
}
async Task ReceiveMessage()
{
logger.LogInformation("Receiving message");
// Big timeout here because it can take a while to receive all the bytes
var receivedData = await connection.Transport.Input.ReadAsync(bytes.Length).OrTimeout(TimeSpan.FromMinutes(2));
Assert.Equal(message, Encoding.UTF8.GetString(receivedData));
logger.LogInformation("Completed receive");
}
// Send the receive concurrently so that back pressure is released
// for server -> client sends
var sendingTask = SendMessage();
var receivingTask = ReceiveMessage();
await sendingTask;
await receivingTask;
} }
catch (Exception ex) catch (Exception ex)
{ {