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:
parent
e89e8d1578
commit
f9a9788c67
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue