Track connections until request processing completes
- Paves the way to allow request processing to continue during server shutdown even after a client disconnects.
This commit is contained in:
parent
c08c57f764
commit
7aa7b3e0a9
|
|
@ -7,7 +7,6 @@ using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
|
|
@ -88,11 +87,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnConnectionClosed()
|
public async void OnConnectionClosed()
|
||||||
{
|
{
|
||||||
_context.ServiceContext.ConnectionManager.RemoveConnection(_context.FrameConnectionId);
|
|
||||||
Log.ConnectionStop(ConnectionId);
|
Log.ConnectionStop(ConnectionId);
|
||||||
KestrelEventSource.Log.ConnectionStop(this);
|
KestrelEventSource.Log.ConnectionStop(this);
|
||||||
|
|
||||||
|
// The connection is already in the "aborted" state by this point, but we want to track it
|
||||||
|
// until RequestProcessingAsync completes for graceful shutdown.
|
||||||
|
await StopAsync();
|
||||||
|
|
||||||
|
_context.ServiceContext.ConnectionManager.RemoveConnection(_context.FrameConnectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
void IHttpRequestLifetimeFeature.Abort()
|
void IHttpRequestLifetimeFeature.Abort()
|
||||||
{
|
{
|
||||||
Abort();
|
Abort(error: null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immediate kill the connection and poison the request and response streams.
|
/// Immediate kill the connection and poison the request and response streams.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Abort(Exception error = null)
|
public void Abort(Exception error)
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _requestAborted, 1) == 0)
|
if (Interlocked.Exchange(ref _requestAborted, 1) == 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
// this is temporary until it does
|
// this is temporary until it does
|
||||||
private TaskCompletionSource<object> _flushTcs;
|
private TaskCompletionSource<object> _flushTcs;
|
||||||
private readonly object _flushLock = new object();
|
private readonly object _flushLock = new object();
|
||||||
private readonly Action _onFlushCallback;
|
|
||||||
|
|
||||||
public OutputProducer(IPipeWriter pipe, Frame frame, string connectionId, IKestrelTrace log)
|
public OutputProducer(IPipeWriter pipe, Frame frame, string connectionId, IKestrelTrace log)
|
||||||
{
|
{
|
||||||
|
|
@ -39,7 +38,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_frame = frame;
|
_frame = frame;
|
||||||
_connectionId = connectionId;
|
_connectionId = connectionId;
|
||||||
_log = log;
|
_log = log;
|
||||||
_onFlushCallback = OnFlush;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteAsync(
|
public Task WriteAsync(
|
||||||
|
|
@ -87,6 +85,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
var awaitable = writableBuffer.FlushAsync();
|
var awaitable = writableBuffer.FlushAsync();
|
||||||
if (awaitable.IsCompleted)
|
if (awaitable.IsCompleted)
|
||||||
{
|
{
|
||||||
|
AbortIfNeeded(awaitable);
|
||||||
|
|
||||||
// The flush task can't fail today
|
// The flush task can't fail today
|
||||||
return TaskCache.CompletedTask;
|
return TaskCache.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
@ -105,16 +105,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_flushTcs = new TaskCompletionSource<object>();
|
_flushTcs = new TaskCompletionSource<object>();
|
||||||
|
|
||||||
awaitable.OnCompleted(_onFlushCallback);
|
awaitable.OnCompleted(() =>
|
||||||
|
{
|
||||||
|
AbortIfNeeded(awaitable);
|
||||||
|
_flushTcs.TrySetResult(null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _flushTcs.Task;
|
return _flushTcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFlush()
|
private void AbortIfNeeded(WritableBufferAwaitable awaitable)
|
||||||
{
|
{
|
||||||
_flushTcs.TrySetResult(null);
|
try
|
||||||
|
{
|
||||||
|
awaitable.GetResult();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_frame.Abort(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ISocketOutput.Write(ArraySegment<byte> buffer, bool chunk)
|
void ISocketOutput.Write(ArraySegment<byte> buffer, bool chunk)
|
||||||
|
|
@ -126,7 +137,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_frame.Abort();
|
_frame.Abort(error: null);
|
||||||
_cancelled = true;
|
_cancelled = true;
|
||||||
return Task.FromCanceled(cancellationToken);
|
return Task.FromCanceled(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
private IConnectionHandler ConnectionHandler => ListenerContext.TransportContext.ConnectionHandler;
|
private IConnectionHandler ConnectionHandler => ListenerContext.TransportContext.ConnectionHandler;
|
||||||
private LibuvThread Thread => ListenerContext.Thread;
|
private LibuvThread Thread => ListenerContext.Thread;
|
||||||
|
|
||||||
public void Start()
|
public async void Start()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -64,45 +64,54 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
ConnectionId = _connectionContext.ConnectionId;
|
ConnectionId = _connectionContext.ConnectionId;
|
||||||
|
|
||||||
Input = _connectionContext.Input;
|
Input = _connectionContext.Input;
|
||||||
Output = new LibuvOutputConsumer(_connectionContext.Output, Thread, _socket, this, ConnectionId, Log);
|
Output = new LibuvOutputConsumer(_connectionContext.Output, Thread, _socket, ConnectionId, Log);
|
||||||
|
|
||||||
// Start socket prior to applying the ConnectionAdapter
|
// Start socket prior to applying the ConnectionAdapter
|
||||||
_socket.ReadStart(_allocCallback, _readCallback, this);
|
_socket.ReadStart(_allocCallback, _readCallback, this);
|
||||||
|
|
||||||
// This *must* happen after socket.ReadStart
|
try
|
||||||
// The socket output consumer is the only thing that can close the connection. If the
|
{
|
||||||
// output pipe is already closed by the time we start then it's fine since, it'll close gracefully afterwards.
|
// This *must* happen after socket.ReadStart
|
||||||
var ignore = Output.StartWrites();
|
// The socket output consumer is the only thing that can close the connection. If the
|
||||||
|
// output pipe is already closed by the time we start then it's fine since, it'll close gracefully afterwards.
|
||||||
|
await Output.WriteOutputAsync();
|
||||||
|
_connectionContext.Output.Complete();
|
||||||
|
}
|
||||||
|
catch (UvException ex)
|
||||||
|
{
|
||||||
|
_connectionContext.Output.Complete(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Ensure the socket is disposed prior to completing in the input writer.
|
||||||
|
_socket.Dispose();
|
||||||
|
Input.Complete(new TaskCanceledException("The request was aborted"));
|
||||||
|
_socketClosedTcs.TrySetResult(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.LogError(0, e, "Connection.StartFrame");
|
Log.LogCritical(0, e, $"{nameof(LibuvConnection)}.{nameof(Start)}() {ConnectionId}");
|
||||||
throw;
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_connectionContext.OnConnectionClosed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StopAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
return Task.WhenAll(_connectionContext.StopAsync(), _socketClosedTcs.Task);
|
await _connectionContext.StopAsync();
|
||||||
|
await _socketClosedTcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual Task AbortAsync(Exception error = null)
|
public Task AbortAsync(Exception error)
|
||||||
{
|
{
|
||||||
_connectionContext.Abort(error);
|
_connectionContext.Abort(error);
|
||||||
return _socketClosedTcs.Task;
|
return StopAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called on Libuv thread
|
// Called on Libuv thread
|
||||||
public virtual void Close()
|
|
||||||
{
|
|
||||||
_socket.Dispose();
|
|
||||||
|
|
||||||
_connectionContext.OnConnectionClosed();
|
|
||||||
|
|
||||||
Input.Complete(new TaskCanceledException("The request was aborted"));
|
|
||||||
_socketClosedTcs.TrySetResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LibuvFunctions.uv_buf_t AllocCallback(UvStreamHandle handle, int suggestedSize, object state)
|
private static LibuvFunctions.uv_buf_t AllocCallback(UvStreamHandle handle, int suggestedSize, object state)
|
||||||
{
|
{
|
||||||
return ((LibuvConnection)state).OnAlloc(handle, suggestedSize);
|
return ((LibuvConnection)state).OnAlloc(handle, suggestedSize);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
|
|
||||||
private void WalkConnectionsAndAbortCore(TaskCompletionSource<object> tcs)
|
private void WalkConnectionsAndAbortCore(TaskCompletionSource<object> tcs)
|
||||||
{
|
{
|
||||||
WalkConnectionsCore(connection => connection.AbortAsync(), tcs);
|
WalkConnectionsCore(connection => connection.AbortAsync(error: null), tcs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WalkConnectionsCore(Func<LibuvConnection, Task> action, TaskCompletionSource<object> tcs)
|
private void WalkConnectionsCore(Func<LibuvConnection, Task> action, TaskCompletionSource<object> tcs)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
{
|
{
|
||||||
private readonly LibuvThread _thread;
|
private readonly LibuvThread _thread;
|
||||||
private readonly UvStreamHandle _socket;
|
private readonly UvStreamHandle _socket;
|
||||||
private readonly LibuvConnection _connection;
|
|
||||||
private readonly string _connectionId;
|
private readonly string _connectionId;
|
||||||
private readonly ILibuvTrace _log;
|
private readonly ILibuvTrace _log;
|
||||||
|
|
||||||
|
|
@ -23,7 +22,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
IPipeReader pipe,
|
IPipeReader pipe,
|
||||||
LibuvThread thread,
|
LibuvThread thread,
|
||||||
UvStreamHandle socket,
|
UvStreamHandle socket,
|
||||||
LibuvConnection connection,
|
|
||||||
string connectionId,
|
string connectionId,
|
||||||
ILibuvTrace log)
|
ILibuvTrace log)
|
||||||
{
|
{
|
||||||
|
|
@ -32,13 +30,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
// get's scheduled
|
// get's scheduled
|
||||||
_thread = thread;
|
_thread = thread;
|
||||||
_socket = socket;
|
_socket = socket;
|
||||||
_connection = connection;
|
|
||||||
_connectionId = connectionId;
|
_connectionId = connectionId;
|
||||||
_log = log;
|
_log = log;
|
||||||
_writeReqPool = thread.WriteReqPool;
|
_writeReqPool = thread.WriteReqPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartWrites()
|
public async Task WriteOutputAsync()
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
|
@ -53,7 +50,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
var writeResult = await writeReq.WriteAsync(_socket, buffer);
|
var writeResult = await writeReq.WriteAsync(_socket, buffer);
|
||||||
_writeReqPool.Return(writeReq);
|
_writeReqPool.Return(writeReq);
|
||||||
|
|
||||||
OnWriteCompleted(writeResult.Status, writeResult.Error);
|
LogWriteInfo(writeResult.Status, writeResult.Error);
|
||||||
|
|
||||||
|
if (writeResult.Error != null)
|
||||||
|
{
|
||||||
|
throw writeResult.Error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.IsCancelled)
|
if (result.IsCancelled)
|
||||||
|
|
@ -74,27 +76,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||||
_pipe.Advance(result.Buffer.End);
|
_pipe.Advance(result.Buffer.End);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're done reading
|
|
||||||
_pipe.Complete();
|
|
||||||
|
|
||||||
// Close the connection
|
|
||||||
_connection.Close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWriteCompleted(int writeStatus, Exception writeError)
|
private void LogWriteInfo(int status, Exception error)
|
||||||
{
|
{
|
||||||
// Called inside _contextLock
|
|
||||||
var status = writeStatus;
|
|
||||||
var error = writeError;
|
|
||||||
|
|
||||||
if (error != null)
|
|
||||||
{
|
|
||||||
// Abort the connection for any failed write
|
|
||||||
// Queued on threadpool so get it in as first op.
|
|
||||||
_connection.AbortAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error == null)
|
if (error == null)
|
||||||
{
|
{
|
||||||
_log.ConnectionWriteCallback(_connectionId, status);
|
_log.ConnectionWriteCallback(_connectionId, status);
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers;
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -316,66 +318,72 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
using (var mockConnection = new MockConnection())
|
var abortedSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var pipeOptions = new PipeOptions
|
||||||
{
|
{
|
||||||
var abortedSource = mockConnection.RequestAbortedSource;
|
ReaderScheduler = _libuvThread,
|
||||||
|
MaximumSizeHigh = maxResponseBufferSize,
|
||||||
|
MaximumSizeLow = maxResponseBufferSize,
|
||||||
|
};
|
||||||
|
|
||||||
var pipeOptions = new PipeOptions
|
using (var socketOutput = CreateOutputProducer(pipeOptions, abortedSource))
|
||||||
|
{
|
||||||
|
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 task2Success = socketOutput.WriteAsync(fullBuffer, default(CancellationToken));
|
||||||
|
var task3Canceled = socketOutput.WriteAsync(fullBuffer, abortedSource.Token);
|
||||||
|
|
||||||
|
// Give time for tasks to percolate
|
||||||
|
await _mockLibuv.OnPostTask;
|
||||||
|
|
||||||
|
// Second task is not completed
|
||||||
|
Assert.False(task2Success.IsCompleted);
|
||||||
|
Assert.False(task2Success.IsCanceled);
|
||||||
|
Assert.False(task2Success.IsFaulted);
|
||||||
|
|
||||||
|
// Third task is not completed
|
||||||
|
Assert.False(task3Canceled.IsCompleted);
|
||||||
|
Assert.False(task3Canceled.IsCanceled);
|
||||||
|
Assert.False(task3Canceled.IsFaulted);
|
||||||
|
|
||||||
|
// Cause all writes to fail
|
||||||
|
while (completeQueue.TryDequeue(out var triggerNextCompleted))
|
||||||
{
|
{
|
||||||
ReaderScheduler = _libuvThread,
|
await _libuvThread.PostAsync(cb => cb(-1), triggerNextCompleted);
|
||||||
MaximumSizeHigh = maxResponseBufferSize,
|
|
||||||
MaximumSizeLow = maxResponseBufferSize,
|
|
||||||
};
|
|
||||||
|
|
||||||
using (var socketOutput = CreateOutputProducer(pipeOptions, mockConnection))
|
|
||||||
{
|
|
||||||
var bufferSize = maxResponseBufferSize - 1;
|
|
||||||
|
|
||||||
var data = new byte[bufferSize];
|
|
||||||
var fullBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var task1Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: 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 task2Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: default(CancellationToken));
|
|
||||||
var task3Canceled = socketOutput.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
|
||||||
|
|
||||||
// Give time for tasks to percolate
|
|
||||||
await _mockLibuv.OnPostTask;
|
|
||||||
|
|
||||||
// Second task is not completed
|
|
||||||
Assert.False(task2Success.IsCompleted);
|
|
||||||
Assert.False(task2Success.IsCanceled);
|
|
||||||
Assert.False(task2Success.IsFaulted);
|
|
||||||
|
|
||||||
// Third task is not completed
|
|
||||||
Assert.False(task3Canceled.IsCompleted);
|
|
||||||
Assert.False(task3Canceled.IsCanceled);
|
|
||||||
Assert.False(task3Canceled.IsFaulted);
|
|
||||||
|
|
||||||
// Cause all writes to fail
|
|
||||||
while (completeQueue.TryDequeue(out var triggerNextCompleted))
|
|
||||||
{
|
|
||||||
await _libuvThread.PostAsync(cb => cb(-1), triggerNextCompleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second task is now completed
|
|
||||||
await task2Success.TimeoutAfter(TimeSpan.FromSeconds(5));
|
|
||||||
|
|
||||||
// Third task is now canceled
|
|
||||||
// TODO: Cancellation isn't supported right now
|
|
||||||
// await Assert.ThrowsAsync<TaskCanceledException>(() => task3Canceled);
|
|
||||||
// Assert.True(task3Canceled.IsCanceled);
|
|
||||||
|
|
||||||
Assert.True(abortedSource.IsCancellationRequested);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Second task is now completed
|
||||||
|
Assert.True(task2Success.IsCompleted);
|
||||||
|
Assert.False(task2Success.IsCanceled);
|
||||||
|
Assert.False(task2Success.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
|
||||||
|
// TODO: Cancellation isn't supported right now
|
||||||
|
// await Assert.ThrowsAsync<TaskCanceledException>(() => task3Canceled);
|
||||||
|
// Assert.True(task3Canceled.IsCanceled);
|
||||||
|
|
||||||
|
Assert.True(abortedSource.IsCancellationRequested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,22 +496,50 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private OutputProducer CreateOutputProducer(PipeOptions pipeOptions, MockConnection connection = null)
|
private OutputProducer CreateOutputProducer(PipeOptions pipeOptions, CancellationTokenSource cts = null)
|
||||||
{
|
{
|
||||||
var pipe = _pipeFactory.Create(pipeOptions);
|
var pipe = _pipeFactory.Create(pipeOptions);
|
||||||
|
|
||||||
var logger = new TestApplicationErrorLogger();
|
var logger = new TestApplicationErrorLogger();
|
||||||
var serviceContext = new TestServiceContext() { Log = new TestKestrelTrace(logger) };
|
var serviceContext = new TestServiceContext
|
||||||
var transportContext = new TestLibuvTransportContext() { Log = new LibuvTrace(logger) };
|
{
|
||||||
|
Log = new TestKestrelTrace(logger),
|
||||||
|
ThreadPool = new InlineLoggingThreadPool(new TestKestrelTrace(logger))
|
||||||
|
};
|
||||||
|
var transportContext = new TestLibuvTransportContext { Log = new LibuvTrace(logger) };
|
||||||
|
|
||||||
var frame = new Frame<object>(null, new FrameContext { ServiceContext = serviceContext });
|
var frame = new Frame<object>(null, new FrameContext { ServiceContext = serviceContext });
|
||||||
|
|
||||||
var socket = new MockSocket(_mockLibuv, _libuvThread.Loop.ThreadId, transportContext.Log);
|
var socket = new MockSocket(_mockLibuv, _libuvThread.Loop.ThreadId, transportContext.Log);
|
||||||
var socketOutput = new OutputProducer(pipe.Writer, frame, "0", serviceContext.Log);
|
var socketOutput = new OutputProducer(pipe.Writer, frame, "0", serviceContext.Log);
|
||||||
var consumer = new LibuvOutputConsumer(pipe.Reader, _libuvThread, socket, connection ?? new MockConnection(), "0", transportContext.Log);
|
var consumer = new LibuvOutputConsumer(pipe.Reader, _libuvThread, socket, "0", transportContext.Log);
|
||||||
var ignore = consumer.StartWrites();
|
|
||||||
|
frame.LifetimeControl = new ConnectionLifetimeControl("0", pipe.Reader, socketOutput, serviceContext.Log);
|
||||||
|
|
||||||
|
if (cts != null)
|
||||||
|
{
|
||||||
|
frame.RequestAborted.Register(cts.Cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ignore = WriteOutputAsync(consumer, pipe.Reader);
|
||||||
|
|
||||||
return socketOutput;
|
return socketOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task WriteOutputAsync(LibuvOutputConsumer consumer, IPipeReader outputReader)
|
||||||
|
{
|
||||||
|
// This WriteOutputAsync() calling code is equivalent to that in LibuvConnection.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure that outputReader.Complete() runs on the LibuvThread.
|
||||||
|
// Without ConfigureAwait(false), xunit will dispatch.
|
||||||
|
await consumer.WriteOutputAsync().ConfigureAwait(false);
|
||||||
|
outputReader.Complete();
|
||||||
|
}
|
||||||
|
catch (UvException ex)
|
||||||
|
{
|
||||||
|
outputReader.Complete(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
// 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;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal;
|
|
||||||
using Microsoft.Extensions.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers
|
|
||||||
{
|
|
||||||
public class MockConnection : LibuvConnection, IDisposable
|
|
||||||
{
|
|
||||||
private readonly TaskCompletionSource<object> _socketClosedTcs = new TaskCompletionSource<object>();
|
|
||||||
|
|
||||||
public MockConnection()
|
|
||||||
{
|
|
||||||
RequestAbortedSource = new CancellationTokenSource();
|
|
||||||
ListenerContext = new ListenerContext(new LibuvTransportContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task AbortAsync(Exception error = null)
|
|
||||||
{
|
|
||||||
RequestAbortedSource?.Cancel();
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Close()
|
|
||||||
{
|
|
||||||
_socketClosedTcs.SetResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CancellationTokenSource RequestAbortedSource { get; }
|
|
||||||
|
|
||||||
public Task SocketClosed => _socketClosedTcs.Task;
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
RequestAbortedSource.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue