Flow the cancellation tokens to ReadAsync and WriteAsync (#2865)
This commit is contained in:
parent
c2685813ab
commit
88273412b8
|
|
@ -148,6 +148,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// TryRead can throw OperationCanceledException https://github.com/dotnet/corefx/issues/32029
|
||||||
|
// beacuse of buggy logic, this works around that for now
|
||||||
|
}
|
||||||
catch (BadHttpRequestException ex)
|
catch (BadHttpRequestException ex)
|
||||||
{
|
{
|
||||||
// At this point, the response has already been written, so this won't result in a 4XX response;
|
// At this point, the response has already been written, so this won't result in a 4XX response;
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state)
|
public Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
|
|
@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_totalBytesCommitted += bytesCommitted;
|
_totalBytesCommitted += bytesCommitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
return FlushAsync();
|
return FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders)
|
public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders)
|
||||||
|
|
|
||||||
|
|
@ -928,7 +928,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
private Task WriteChunkedAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
private Task WriteChunkedAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return Output.WriteAsync(_writeChunk, data);
|
return Output.WriteAsync(_writeChunk, data, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long WriteChunk(PipeWriter writableBuffer, ReadOnlyMemory<byte> buffer)
|
private static long WriteChunk(PipeWriter writableBuffer, ReadOnlyMemory<byte> buffer)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
public interface IHttpOutputProducer : IDisposable
|
public interface IHttpOutputProducer : IDisposable
|
||||||
{
|
{
|
||||||
void Abort(ConnectionAbortedException abortReason);
|
void Abort(ConnectionAbortedException abortReason);
|
||||||
Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state);
|
Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state, CancellationToken cancellationToken);
|
||||||
Task FlushAsync(CancellationToken cancellationToken);
|
Task FlushAsync(CancellationToken cancellationToken);
|
||||||
Task Write100ContinueAsync(CancellationToken cancellationToken);
|
Task Write100ContinueAsync(CancellationToken cancellationToken);
|
||||||
void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders);
|
void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var result = await _context.RequestBodyPipe.Reader.ReadAsync();
|
var result = await _context.RequestBodyPipe.Reader.ReadAsync(cancellationToken);
|
||||||
var readableBuffer = result.Buffer;
|
var readableBuffer = result.Buffer;
|
||||||
var consumed = readableBuffer.End;
|
var consumed = readableBuffer.End;
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var result = await _context.RequestBodyPipe.Reader.ReadAsync();
|
var result = await _context.RequestBodyPipe.Reader.ReadAsync(cancellationToken);
|
||||||
var readableBuffer = result.Buffer;
|
var readableBuffer = result.Buffer;
|
||||||
var consumed = readableBuffer.End;
|
var consumed = readableBuffer.End;
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
// - The WriteAsync(ReadOnlyMemory<byte>) isn't overridden on the destination
|
// - The WriteAsync(ReadOnlyMemory<byte>) isn't overridden on the destination
|
||||||
// - We change the Kestrel Memory Pool to not use pinned arrays but instead use native memory
|
// - We change the Kestrel Memory Pool to not use pinned arrays but instead use native memory
|
||||||
#if NETCOREAPP2_1
|
#if NETCOREAPP2_1
|
||||||
await destination.WriteAsync(memory);
|
await destination.WriteAsync(memory, cancellationToken);
|
||||||
#else
|
#else
|
||||||
var array = memory.GetArray();
|
var array = memory.GetArray();
|
||||||
await destination.WriteAsync(array.Array, array.Offset, array.Count, cancellationToken);
|
await destination.WriteAsync(array.Array, array.Offset, array.Count, cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
// TODO: RST_STREAM?
|
// TODO: RST_STREAM?
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state)
|
public Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,74 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestBodyReadAsyncCanBeCancelled()
|
||||||
|
{
|
||||||
|
var helloTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var readTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
using (var server = new TestServer(async context =>
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.Request.Body.ReadUntilLengthAsync(buffer, 6, cts.Token).DefaultTimeout();
|
||||||
|
|
||||||
|
Assert.Equal("Hello ", Encoding.ASCII.GetString(buffer, 0, 6));
|
||||||
|
|
||||||
|
helloTcs.TrySetResult(null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// This shouldn't fail
|
||||||
|
helloTcs.TrySetException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var task = context.Request.Body.ReadAsync(buffer, 0, buffer.Length, cts.Token);
|
||||||
|
readTcs.TrySetResult(null);
|
||||||
|
await task;
|
||||||
|
|
||||||
|
context.Response.ContentLength = 12;
|
||||||
|
await context.Response.WriteAsync("Read success");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
context.Response.ContentLength = 14;
|
||||||
|
await context.Response.WriteAsync("Read cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
}, new TestServiceContext(LoggerFactory)))
|
||||||
|
{
|
||||||
|
using (var connection = server.CreateConnection())
|
||||||
|
{
|
||||||
|
await connection.Send(
|
||||||
|
"POST / HTTP/1.1",
|
||||||
|
"Host:",
|
||||||
|
"Connection: keep-alive",
|
||||||
|
"Content-Length: 11",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
await connection.Send("Hello ");
|
||||||
|
|
||||||
|
await helloTcs.Task;
|
||||||
|
await readTcs.Task;
|
||||||
|
|
||||||
|
// Cancel the body after hello is read
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
await connection.Receive($"HTTP/1.1 200 OK",
|
||||||
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
|
"Content-Length: 14",
|
||||||
|
"",
|
||||||
|
"Read cancelled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanUpgradeRequestWithConnectionKeepAliveUpgradeHeader()
|
public void CanUpgradeRequestWithConnectionKeepAliveUpgradeHeader()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,73 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyWriteAsyncCanBeCancelled()
|
||||||
|
{
|
||||||
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var appTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var writeBlockedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async context =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("hello", cts.Token).DefaultTimeout();
|
||||||
|
|
||||||
|
var data = new byte[1024 * 1024 * 10];
|
||||||
|
|
||||||
|
var timerTask = Task.Delay(TimeSpan.FromSeconds(1));
|
||||||
|
var writeTask = context.Response.Body.WriteAsync(data, 0, data.Length, cts.Token).DefaultTimeout();
|
||||||
|
var completedTask = await Task.WhenAny(writeTask, timerTask);
|
||||||
|
|
||||||
|
while (completedTask == writeTask)
|
||||||
|
{
|
||||||
|
await writeTask;
|
||||||
|
timerTask = Task.Delay(TimeSpan.FromSeconds(1));
|
||||||
|
writeTask = context.Response.Body.WriteAsync(data, 0, data.Length, cts.Token).DefaultTimeout();
|
||||||
|
completedTask = await Task.WhenAny(writeTask, timerTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeBlockedTcs.TrySetResult(null);
|
||||||
|
|
||||||
|
await writeTask;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
appTcs.TrySetException(ex);
|
||||||
|
writeBlockedTcs.TrySetException(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
appTcs.TrySetResult(null);
|
||||||
|
}
|
||||||
|
}, serviceContext))
|
||||||
|
{
|
||||||
|
using (var connection = server.CreateConnection())
|
||||||
|
{
|
||||||
|
await connection.Send(
|
||||||
|
"GET / HTTP/1.1",
|
||||||
|
"Host:",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
await connection.Receive($"HTTP/1.1 200 OK",
|
||||||
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
|
"Transfer-Encoding: chunked",
|
||||||
|
"",
|
||||||
|
"5",
|
||||||
|
"hello");
|
||||||
|
|
||||||
|
await writeBlockedTcs.Task.DefaultTimeout();
|
||||||
|
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<OperationCanceledException>(() => appTcs.Task).DefaultTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task ResponseStatusCodeSetBeforeHttpContextDisposeAppException()
|
public Task ResponseStatusCodeSetBeforeHttpContextDisposeAppException()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace System.IO
|
||||||
|
{
|
||||||
|
public static class StreamFillBufferExtensions
|
||||||
|
{
|
||||||
|
public static async Task<int> ReadUntilEndAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
while (offset < buffer.Length)
|
||||||
|
{
|
||||||
|
var read = await stream.ReadAsync(buffer, offset, buffer.Length - offset, cancellationToken);
|
||||||
|
offset += read;
|
||||||
|
|
||||||
|
if (read == 0)
|
||||||
|
{
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1, cancellationToken));
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task ReadUntilLengthAsync(this Stream stream, byte[] buffer, int length, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
while (offset < length)
|
||||||
|
{
|
||||||
|
var read = await stream.ReadAsync(buffer, offset, length - offset, cancellationToken);
|
||||||
|
offset += read;
|
||||||
|
|
||||||
|
Assert.NotEqual(0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Https" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Https" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
|
||||||
<Reference Include="Newtonsoft.Json" />
|
<Reference Include="Newtonsoft.Json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Https" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Https" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
|
||||||
<Reference Include="Newtonsoft.Json" />
|
<Reference Include="Newtonsoft.Json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue