Add cancellation support for write async (#1736)
This commit is contained in:
parent
71d2abed06
commit
df9e48b1f0
|
|
@ -12,8 +12,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||||
{
|
{
|
||||||
public class StreamSocketOutput : ISocketOutput
|
public class StreamSocketOutput : ISocketOutput
|
||||||
{
|
{
|
||||||
private static readonly ArraySegment<byte> _nullBuffer = new ArraySegment<byte>(new byte[0]);
|
|
||||||
|
|
||||||
private readonly Stream _outputStream;
|
private readonly Stream _outputStream;
|
||||||
private readonly IPipe _pipe;
|
private readonly IPipe _pipe;
|
||||||
private readonly object _sync = new object();
|
private readonly object _sync = new object();
|
||||||
|
|
@ -57,7 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flushAwaiter = writableBuffer.FlushAsync();
|
flushAwaiter = writableBuffer.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await flushAwaiter;
|
await flushAwaiter;
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
writableBuffer.Commit();
|
writableBuffer.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
return FlushAsync(writableBuffer);
|
return FlushAsync(writableBuffer, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task FlushAsync(WritableBuffer writableBuffer)
|
private Task FlushAsync(WritableBuffer writableBuffer,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var awaitable = writableBuffer.FlushAsync();
|
var awaitable = writableBuffer.FlushAsync(cancellationToken);
|
||||||
if (awaitable.IsCompleted)
|
if (awaitable.IsCompleted)
|
||||||
{
|
{
|
||||||
AbortIfNeeded(awaitable);
|
AbortIfNeeded(awaitable);
|
||||||
|
|
@ -90,10 +91,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
// The flush task can't fail today
|
// The flush task can't fail today
|
||||||
return TaskCache.CompletedTask;
|
return TaskCache.CompletedTask;
|
||||||
}
|
}
|
||||||
return FlushAsyncAwaited(awaitable);
|
return FlushAsyncAwaited(awaitable, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task FlushAsyncAwaited(WritableBufferAwaitable awaitable)
|
private async Task FlushAsyncAwaited(WritableBufferAwaitable awaitable, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// https://github.com/dotnet/corefxlab/issues/1334
|
// https://github.com/dotnet/corefxlab/issues/1334
|
||||||
// Since the flush awaitable doesn't currently support multiple awaiters
|
// Since the flush awaitable doesn't currently support multiple awaiters
|
||||||
|
|
@ -112,8 +113,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await _flushTcs.Task;
|
||||||
return _flushTcs.Task;
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AbortIfNeeded(WritableBufferAwaitable awaitable)
|
private void AbortIfNeeded(WritableBufferAwaitable awaitable)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
{
|
{
|
||||||
var result = await _pipe.ReadAsync();
|
var result = await _pipe.ReadAsync();
|
||||||
var buffer = result.Buffer;
|
var buffer = result.Buffer;
|
||||||
|
var consumed = buffer.End;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -53,6 +54,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
|
|
||||||
if (writeResult.Error != null)
|
if (writeResult.Error != null)
|
||||||
{
|
{
|
||||||
|
consumed = buffer.Start;
|
||||||
throw writeResult.Error;
|
throw writeResult.Error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_pipe.Advance(result.Buffer.End);
|
_pipe.Advance(consumed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -382,14 +382,159 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
||||||
Assert.False(task4Success.IsFaulted);
|
Assert.False(task4Success.IsFaulted);
|
||||||
|
|
||||||
// Third task is now canceled
|
// Third task is now canceled
|
||||||
// TODO: Cancellation isn't supported right now
|
await Assert.ThrowsAsync<OperationCanceledException>(() => task3Canceled);
|
||||||
// await Assert.ThrowsAsync<TaskCanceledException>(() => task3Canceled);
|
Assert.True(task3Canceled.IsCanceled);
|
||||||
// Assert.True(task3Canceled.IsCanceled);
|
|
||||||
|
|
||||||
Assert.True(abortedSource.IsCancellationRequested);
|
Assert.True(abortedSource.IsCancellationRequested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(PositiveMaxResponseBufferSizeData))]
|
||||||
|
public async Task CancelsBeforeWriteRequestCompletes(int maxResponseBufferSize)
|
||||||
|
{
|
||||||
|
var completeQueue = new ConcurrentQueue<Action<int>>();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
||||||
|
{
|
||||||
|
completeQueue.Enqueue(triggerCompleted);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
var abortedSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var pipeOptions = new PipeOptions
|
||||||
|
{
|
||||||
|
ReaderScheduler = _libuvThread,
|
||||||
|
MaximumSizeHigh = maxResponseBufferSize,
|
||||||
|
MaximumSizeLow = maxResponseBufferSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||||
|
{
|
||||||
|
var bufferSize = maxResponseBufferSize - 1;
|
||||||
|
|
||||||
|
var data = new byte[bufferSize];
|
||||||
|
var fullBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task1Success = socketOutput.WriteAsync(fullBuffer, abortedSource.Token);
|
||||||
|
// task1 should complete successfully as < _maxBytesPreCompleted
|
||||||
|
|
||||||
|
// First task is completed and successful
|
||||||
|
Assert.True(task1Success.IsCompleted);
|
||||||
|
Assert.False(task1Success.IsCanceled);
|
||||||
|
Assert.False(task1Success.IsFaulted);
|
||||||
|
|
||||||
|
// following tasks should wait.
|
||||||
|
var task3Canceled = socketOutput.WriteAsync(fullBuffer, abortedSource.Token);
|
||||||
|
|
||||||
|
// Give time for tasks to percolate
|
||||||
|
await _mockLibuv.OnPostTask;
|
||||||
|
|
||||||
|
// Third task is not completed
|
||||||
|
Assert.False(task3Canceled.IsCompleted);
|
||||||
|
Assert.False(task3Canceled.IsCanceled);
|
||||||
|
Assert.False(task3Canceled.IsFaulted);
|
||||||
|
|
||||||
|
abortedSource.Cancel();
|
||||||
|
|
||||||
|
// Complete writes
|
||||||
|
while (completeQueue.TryDequeue(out var triggerNextCompleted))
|
||||||
|
{
|
||||||
|
await _libuvThread.PostAsync(cb => cb(0), triggerNextCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A final write guarantees that the error is observed by OutputProducer,
|
||||||
|
// but doesn't return a canceled/faulted task.
|
||||||
|
var task4Success = socketOutput.WriteAsync(fullBuffer, default(CancellationToken));
|
||||||
|
Assert.True(task4Success.IsCompleted);
|
||||||
|
Assert.False(task4Success.IsCanceled);
|
||||||
|
Assert.False(task4Success.IsFaulted);
|
||||||
|
|
||||||
|
// Third task is now canceled
|
||||||
|
await Assert.ThrowsAsync<OperationCanceledException>(() => task3Canceled);
|
||||||
|
Assert.True(task3Canceled.IsCanceled);
|
||||||
|
|
||||||
|
Assert.True(abortedSource.IsCancellationRequested);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(PositiveMaxResponseBufferSizeData))]
|
||||||
|
public async Task WriteAsyncWithTokenAfterCallWithoutIsCancelled(int maxResponseBufferSize)
|
||||||
|
{
|
||||||
|
var completeQueue = new ConcurrentQueue<Action<int>>();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
||||||
|
{
|
||||||
|
completeQueue.Enqueue(triggerCompleted);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
var abortedSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var pipeOptions = new PipeOptions
|
||||||
|
{
|
||||||
|
ReaderScheduler = _libuvThread,
|
||||||
|
MaximumSizeHigh = maxResponseBufferSize,
|
||||||
|
MaximumSizeLow = maxResponseBufferSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||||
|
{
|
||||||
|
var bufferSize = maxResponseBufferSize;
|
||||||
|
|
||||||
|
var data = new byte[bufferSize];
|
||||||
|
var fullBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task1Waits = socketOutput.WriteAsync(fullBuffer, default(CancellationToken));
|
||||||
|
|
||||||
|
// First task is not completed
|
||||||
|
Assert.False(task1Waits.IsCompleted);
|
||||||
|
Assert.False(task1Waits.IsCanceled);
|
||||||
|
Assert.False(task1Waits.IsFaulted);
|
||||||
|
|
||||||
|
// following tasks should wait.
|
||||||
|
var task3Canceled = socketOutput.WriteAsync(fullBuffer, abortedSource.Token);
|
||||||
|
|
||||||
|
// Give time for tasks to percolate
|
||||||
|
await _mockLibuv.OnPostTask;
|
||||||
|
|
||||||
|
// Third task is not completed
|
||||||
|
Assert.False(task3Canceled.IsCompleted);
|
||||||
|
Assert.False(task3Canceled.IsCanceled);
|
||||||
|
Assert.False(task3Canceled.IsFaulted);
|
||||||
|
|
||||||
|
abortedSource.Cancel();
|
||||||
|
|
||||||
|
// Complete writes
|
||||||
|
while (completeQueue.TryDequeue(out var triggerNextCompleted))
|
||||||
|
{
|
||||||
|
await _libuvThread.PostAsync(cb => cb(0), triggerNextCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First task is completed
|
||||||
|
Assert.True(task1Waits.IsCompleted);
|
||||||
|
Assert.False(task1Waits.IsCanceled);
|
||||||
|
Assert.False(task1Waits.IsFaulted);
|
||||||
|
|
||||||
|
// A final write guarantees that the error is observed by OutputProducer,
|
||||||
|
// but doesn't return a canceled/faulted task.
|
||||||
|
var task4Success = socketOutput.WriteAsync(fullBuffer, default(CancellationToken));
|
||||||
|
Assert.True(task4Success.IsCompleted);
|
||||||
|
Assert.False(task4Success.IsCanceled);
|
||||||
|
Assert.False(task4Success.IsFaulted);
|
||||||
|
|
||||||
|
// Third task is now canceled
|
||||||
|
await Assert.ThrowsAsync<OperationCanceledException>(() => task3Canceled);
|
||||||
|
Assert.True(task3Canceled.IsCanceled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(PositiveMaxResponseBufferSizeData))]
|
[MemberData(nameof(PositiveMaxResponseBufferSizeData))]
|
||||||
public async Task WritesDontGetCompletedTooQuickly(int maxResponseBufferSize)
|
public async Task WritesDontGetCompletedTooQuickly(int maxResponseBufferSize)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue