SocketOutput Cancellation handling

This commit is contained in:
Ben Adams 2016-01-15 13:08:40 +00:00
parent b062f851dc
commit 73bb0ab5b8
16 changed files with 421 additions and 108 deletions

View File

@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
private bool _requestProcessingStarted; private bool _requestProcessingStarted;
private Task _requestProcessingTask; private Task _requestProcessingTask;
protected volatile bool _requestProcessingStopping; // volatile, see: https://msdn.microsoft.com/en-us/library/x13ttww7.aspx protected volatile bool _requestProcessingStopping; // volatile, see: https://msdn.microsoft.com/en-us/library/x13ttww7.aspx
protected volatile bool _requestAborted; protected int _requestAborted;
protected CancellationTokenSource _abortedCts; protected CancellationTokenSource _abortedCts;
protected CancellationToken? _manuallySetRequestAbortToken; protected CancellationToken? _manuallySetRequestAbortToken;
@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
var cts = _abortedCts; var cts = _abortedCts;
return return
cts != null ? cts.Token : cts != null ? cts.Token :
_requestAborted ? new CancellationToken(true) : (Volatile.Read(ref _requestAborted) == 1) ? new CancellationToken(true) :
RequestAbortedSource.Token; RequestAbortedSource.Token;
} }
set set
@ -185,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
// Get the abort token, lazily-initializing it if necessary. // Get the abort token, lazily-initializing it if necessary.
// Make sure it's canceled if an abort request already came in. // Make sure it's canceled if an abort request already came in.
var cts = LazyInitializer.EnsureInitialized(ref _abortedCts, () => new CancellationTokenSource()); var cts = LazyInitializer.EnsureInitialized(ref _abortedCts, () => new CancellationTokenSource());
if (_requestAborted) if (Volatile.Read(ref _requestAborted) == 1)
{ {
cts.Cancel(); cts.Cancel();
} }
@ -288,24 +288,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
/// </summary> /// </summary>
public void Abort() public void Abort()
{ {
_requestProcessingStopping = true; if (Interlocked.CompareExchange(ref _requestAborted, 1, 0) == 0)
_requestAborted = true; {
_requestProcessingStopping = true;
_requestBody?.Abort(); _requestBody?.Abort();
_responseBody?.Abort(); _responseBody?.Abort();
try try
{ {
ConnectionControl.End(ProduceEndType.SocketDisconnect); ConnectionControl.End(ProduceEndType.SocketDisconnect);
SocketInput.AbortAwaiting(); SocketInput.AbortAwaiting();
RequestAbortedSource.Cancel(); }
} catch (Exception ex)
catch (Exception ex) {
{ Log.LogError("Abort", ex);
Log.LogError("Abort", ex); }
}
finally try
{ {
RequestAbortedSource.Cancel();
}
catch (Exception ex)
{
Log.LogError("Abort", ex);
}
_abortedCts = null; _abortedCts = null;
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Net; using System.Net;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
@ -111,7 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
_application.DisposeContext(context, _applicationException); _application.DisposeContext(context, _applicationException);
// If _requestAbort is set, the connection has already been closed. // If _requestAbort is set, the connection has already been closed.
if (!_requestAborted) if (Volatile.Read(ref _requestAborted) == 0)
{ {
_responseBody.ResumeAcceptingWrites(); _responseBody.ResumeAcceptingWrites();
await ProduceEnd(); await ProduceEnd();
@ -148,7 +149,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
_abortedCts = null; _abortedCts = null;
// If _requestAborted is set, the connection has already been closed. // If _requestAborted is set, the connection has already been closed.
if (!_requestAborted) if (Volatile.Read(ref _requestAborted) == 0)
{ {
// Inform client no more data will ever arrive // Inform client no more data will ever arrive
ConnectionControl.End(ProduceEndType.SocketShutdownSend); ConnectionControl.End(ProduceEndType.SocketShutdownSend);

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Server.Kestrel.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Http namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {
@ -51,8 +52,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
ValidateState();
// ValueTask uses .GetAwaiter().GetResult() if necessary // ValueTask uses .GetAwaiter().GetResult() if necessary
return ReadAsync(buffer, offset, count).Result; return ReadAsync(buffer, offset, count).Result;
} }
@ -60,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
#if NET451 #if NET451
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{ {
ValidateState(); ValidateState(CancellationToken.None);
var task = ReadAsync(buffer, offset, count, CancellationToken.None, state); var task = ReadAsync(buffer, offset, count, CancellationToken.None, state);
if (callback != null) if (callback != null)
@ -77,7 +76,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
private Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) private Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state)
{ {
ValidateState(); ValidateState(cancellationToken);
var tcs = new TaskCompletionSource<int>(state); var tcs = new TaskCompletionSource<int>(state);
var task = _body.ReadAsync(new ArraySegment<byte>(buffer, offset, count), cancellationToken); var task = _body.ReadAsync(new ArraySegment<byte>(buffer, offset, count), cancellationToken);
@ -103,10 +102,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{ {
ValidateState(); var task = ValidateState(cancellationToken);
if (task == null)
// Needs .AsTask to match Stream's Async method return types {
return _body.ReadAsync(new ArraySegment<byte>(buffer, offset, count), cancellationToken).AsTask(); // Needs .AsTask to match Stream's Async method return types
return _body.ReadAsync(new ArraySegment<byte>(buffer, offset, count), cancellationToken).AsTask();
}
return task;
} }
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
@ -149,24 +151,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public void Abort() public void Abort()
{ {
// We don't want to throw an ODE until the app func actually completes. // We don't want to throw an ODE until the app func actually completes.
// If the request is aborted, we throw an IOException instead. // If the request is aborted, we throw an TaskCanceledException instead.
if (_state != FrameStreamState.Closed) if (_state != FrameStreamState.Closed)
{ {
_state = FrameStreamState.Aborted; _state = FrameStreamState.Aborted;
} }
} }
private void ValidateState() private Task<int> ValidateState(CancellationToken cancellationToken)
{ {
switch (_state) switch (_state)
{ {
case FrameStreamState.Open: case FrameStreamState.Open:
return; if (cancellationToken.IsCancellationRequested)
{
return TaskUtilities.GetCancelledZeroTask();
}
break;
case FrameStreamState.Closed: case FrameStreamState.Closed:
throw new ObjectDisposedException(nameof(FrameRequestStream)); throw new ObjectDisposedException(nameof(FrameRequestStream));
case FrameStreamState.Aborted: case FrameStreamState.Aborted:
throw new IOException("The request has been aborted."); return TaskUtilities.GetCancelledZeroTask();
} }
return null;
} }
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Server.Kestrel.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Http namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {
@ -37,16 +38,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public override void Flush() public override void Flush()
{ {
ValidateState(); ValidateState(CancellationToken.None);
_context.FrameControl.Flush(); _context.FrameControl.Flush();
} }
public override Task FlushAsync(CancellationToken cancellationToken) public override Task FlushAsync(CancellationToken cancellationToken)
{ {
ValidateState(); var task = ValidateState(cancellationToken);
if (task == null)
return _context.FrameControl.FlushAsync(cancellationToken); {
return _context.FrameControl.FlushAsync(cancellationToken);
}
return task;
} }
public override long Seek(long offset, SeekOrigin origin) public override long Seek(long offset, SeekOrigin origin)
@ -66,16 +70,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
{ {
ValidateState(); ValidateState(CancellationToken.None);
_context.FrameControl.Write(new ArraySegment<byte>(buffer, offset, count)); _context.FrameControl.Write(new ArraySegment<byte>(buffer, offset, count));
} }
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{ {
ValidateState(); var task = ValidateState(cancellationToken);
if (task == null)
return _context.FrameControl.WriteAsync(new ArraySegment<byte>(buffer, offset, count), cancellationToken); {
return _context.FrameControl.WriteAsync(new ArraySegment<byte>(buffer, offset, count), cancellationToken);
}
return task;
} }
public Stream StartAcceptingWrites() public Stream StartAcceptingWrites()
@ -112,24 +119,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public void Abort() public void Abort()
{ {
// We don't want to throw an ODE until the app func actually completes. // We don't want to throw an ODE until the app func actually completes.
// If the request is aborted, we throw an IOException instead.
if (_state != FrameStreamState.Closed) if (_state != FrameStreamState.Closed)
{ {
_state = FrameStreamState.Aborted; _state = FrameStreamState.Aborted;
} }
} }
private void ValidateState() private Task ValidateState(CancellationToken cancellationToken)
{ {
switch (_state) switch (_state)
{ {
case FrameStreamState.Open: case FrameStreamState.Open:
return; if (cancellationToken.IsCancellationRequested)
{
return TaskUtilities.GetCancelledTask(cancellationToken);
}
break;
case FrameStreamState.Closed: case FrameStreamState.Closed:
throw new ObjectDisposedException(nameof(FrameResponseStream)); throw new ObjectDisposedException(nameof(FrameResponseStream));
case FrameStreamState.Aborted: case FrameStreamState.Aborted:
throw new IOException("The request has been aborted."); if (cancellationToken.CanBeCanceled)
{
// Aborted state only throws on write if cancellationToken requests it
return TaskUtilities.GetCancelledTask(
cancellationToken.IsCancellationRequested ?
cancellationToken :
new CancellationToken(true));
}
break;
} }
return null;
} }
} }
} }

View File

@ -2,11 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Http namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Http namespace Microsoft.AspNetCore.Server.Kestrel.Http
@ -184,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public void AbortAwaiting() public void AbortAwaiting()
{ {
_awaitableError = new ObjectDisposedException(nameof(SocketInput), "The request was aborted"); _awaitableError = new TaskCanceledException("The request was aborted");
Complete(); Complete();
} }
@ -238,6 +239,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
var error = _awaitableError; var error = _awaitableError;
if (error != null) if (error != null)
{ {
if (error is TaskCanceledException || error is InvalidOperationException)
{
throw error;
}
throw new IOException(error.Message, error); throw new IOException(error.Message, error);
} }
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
@ -22,6 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
private const int _maxPooledWriteContexts = 32; private const int _maxPooledWriteContexts = 32;
private static readonly WaitCallback _returnBlocks = (state) => ReturnBlocks((MemoryPoolBlock2)state); private static readonly WaitCallback _returnBlocks = (state) => ReturnBlocks((MemoryPoolBlock2)state);
private static readonly Action<object> _connectionCancellation = (state) => ((SocketOutput)state).CancellationTriggered();
private readonly KestrelThread _thread; private readonly KestrelThread _thread;
private readonly UvStreamHandle _socket; private readonly UvStreamHandle _socket;
@ -78,6 +78,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public Task WriteAsync( public Task WriteAsync(
ArraySegment<byte> buffer, ArraySegment<byte> buffer,
CancellationToken cancellationToken,
bool immediate = true, bool immediate = true,
bool chunk = false, bool chunk = false,
bool socketShutdownSend = false, bool socketShutdownSend = false,
@ -89,9 +90,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
lock (_contextLock) lock (_contextLock)
{ {
if (_lastWriteError != null || _socket.IsClosed)
{
_log.ConnectionDisconnectedWrite(_connectionId, buffer.Count, _lastWriteError);
return TaskUtilities.CompletedTask;
}
if (buffer.Count > 0) if (buffer.Count > 0)
{ {
var tail = ProducingStart(); var tail = ProducingStart();
if (tail.IsDefault)
{
return TaskUtilities.CompletedTask;
}
if (chunk) if (chunk)
{ {
_numBytesPreCompleted += ChunkWriter.WriteBeginChunkBytes(ref tail, buffer.Count); _numBytesPreCompleted += ChunkWriter.WriteBeginChunkBytes(ref tail, buffer.Count);
@ -146,13 +159,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
} }
else else
{ {
// immediate write, which is not eligable for instant completion above if (cancellationToken.CanBeCanceled)
tcs = new TaskCompletionSource<object>(buffer.Count); {
_tasksPending.Enqueue(new WaitingTask() { if (cancellationToken.IsCancellationRequested)
CompletionSource = tcs, {
BytesToWrite = buffer.Count, _connection.Abort();
IsSync = isSync
}); return TaskUtilities.GetCancelledTask(cancellationToken);
}
else
{
// immediate write, which is not eligable for instant completion above
tcs = new TaskCompletionSource<object>();
_tasksPending.Enqueue(new WaitingTask()
{
CancellationToken = cancellationToken,
CancellationRegistration = cancellationToken.Register(_connectionCancellation, this),
BytesToWrite = buffer.Count,
CompletionSource = tcs
});
}
}
else
{
tcs = new TaskCompletionSource<object>();
_tasksPending.Enqueue(new WaitingTask() {
IsSync = isSync,
BytesToWrite = buffer.Count,
CompletionSource = tcs
});
}
} }
if (!_writePending && immediate) if (!_writePending && immediate)
@ -177,12 +213,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {
case ProduceEndType.SocketShutdownSend: case ProduceEndType.SocketShutdownSend:
WriteAsync(default(ArraySegment<byte>), WriteAsync(default(ArraySegment<byte>),
default(CancellationToken),
immediate: true, immediate: true,
socketShutdownSend: true, socketShutdownSend: true,
socketDisconnect: false); socketDisconnect: false);
break; break;
case ProduceEndType.SocketDisconnect: case ProduceEndType.SocketDisconnect:
WriteAsync(default(ArraySegment<byte>), WriteAsync(default(ArraySegment<byte>),
default(CancellationToken),
immediate: true, immediate: true,
socketShutdownSend: false, socketShutdownSend: false,
socketDisconnect: true); socketDisconnect: true);
@ -198,7 +236,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
if (_tail == null) if (_tail == null)
{ {
throw new IOException("The socket has been closed."); return default(MemoryPoolIterator2);
} }
_lastStart = new MemoryPoolIterator2(_tail, _tail.End); _lastStart = new MemoryPoolIterator2(_tail, _tail.End);
@ -251,6 +289,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
} }
} }
private void CancellationTriggered()
{
lock (_contextLock)
{
// Abort the connection for any failed write
// Queued on threadpool so get it in as first op.
_connection?.Abort();
CompleteAllWrites();
}
}
private static void ReturnBlocks(MemoryPoolBlock2 block) private static void ReturnBlocks(MemoryPoolBlock2 block)
{ {
while (block != null) while (block != null)
@ -305,10 +355,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
if (error != null) if (error != null)
{ {
_lastWriteError = new IOException(error.Message, error); // Abort the connection for any failed write
// Queued on threadpool so get it in as first op.
// Abort the connection for any failed write.
_connection.Abort(); _connection.Abort();
_lastWriteError = error;
} }
PoolWriteContext(writeContext); PoolWriteContext(writeContext);
@ -317,43 +367,78 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
// completed writes that we haven't triggered callbacks for yet. // completed writes that we haven't triggered callbacks for yet.
_numBytesPreCompleted -= bytesWritten; _numBytesPreCompleted -= bytesWritten;
CompleteFinishedWrites(status);
if (error != null)
{
_log.ConnectionError(_connectionId, error);
}
else
{
_log.ConnectionWriteCallback(_connectionId, status);
}
}
private void CompleteNextWrite(ref int bytesLeftToBuffer)
{
var waitingTask = _tasksPending.Dequeue();
var bytesToWrite = waitingTask.BytesToWrite;
_numBytesPreCompleted += bytesToWrite;
bytesLeftToBuffer -= bytesToWrite;
// Dispose registration if there is one
waitingTask.CancellationRegistration?.Dispose();
if (waitingTask.CancellationToken.IsCancellationRequested)
{
if (waitingTask.IsSync)
{
waitingTask.CompletionSource.TrySetCanceled();
}
else
{
_threadPool.Cancel(waitingTask.CompletionSource);
}
}
else
{
if (waitingTask.IsSync)
{
waitingTask.CompletionSource.TrySetResult(null);
}
else
{
_threadPool.Complete(waitingTask.CompletionSource);
}
}
}
private void CompleteFinishedWrites(int status)
{
// bytesLeftToBuffer can be greater than _maxBytesPreCompleted // bytesLeftToBuffer can be greater than _maxBytesPreCompleted
// This allows large writes to complete once they've actually finished. // This allows large writes to complete once they've actually finished.
var bytesLeftToBuffer = _maxBytesPreCompleted - _numBytesPreCompleted; var bytesLeftToBuffer = _maxBytesPreCompleted - _numBytesPreCompleted;
while (_tasksPending.Count > 0 && while (_tasksPending.Count > 0 &&
(_tasksPending.Peek().BytesToWrite) <= bytesLeftToBuffer) (_tasksPending.Peek().BytesToWrite) <= bytesLeftToBuffer)
{ {
var waitingTask = _tasksPending.Dequeue(); CompleteNextWrite(ref bytesLeftToBuffer);
var bytesToWrite = waitingTask.BytesToWrite; }
}
_numBytesPreCompleted += bytesToWrite; private void CompleteAllWrites()
bytesLeftToBuffer -= bytesToWrite; {
var writesToComplete = _tasksPending.Count > 0;
if (_lastWriteError == null) var bytesLeftToBuffer = _maxBytesPreCompleted - _numBytesPreCompleted;
{ while (_tasksPending.Count > 0)
if (waitingTask.IsSync) {
{ CompleteNextWrite(ref bytesLeftToBuffer);
waitingTask.CompletionSource.TrySetResult(null);
}
else
{
_threadPool.Complete(waitingTask.CompletionSource);
}
}
else
{
if (waitingTask.IsSync)
{
waitingTask.CompletionSource.TrySetException(_lastWriteError);
}
else
{
_threadPool.Error(waitingTask.CompletionSource, _lastWriteError);
}
}
} }
_log.ConnectionWriteCallback(_connectionId, status); if (writesToComplete)
{
_log.ConnectionError(_connectionId, new TaskCanceledException("Connetcion"));
}
} }
// This is called on the libuv event loop // This is called on the libuv event loop
@ -393,12 +478,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
void ISocketOutput.Write(ArraySegment<byte> buffer, bool immediate, bool chunk) void ISocketOutput.Write(ArraySegment<byte> buffer, bool immediate, bool chunk)
{ {
WriteAsync(buffer, immediate, chunk, isSync: true).GetAwaiter().GetResult(); WriteAsync(buffer, CancellationToken.None, immediate, chunk, isSync: true).GetAwaiter().GetResult();
} }
Task ISocketOutput.WriteAsync(ArraySegment<byte> buffer, bool immediate, bool chunk, CancellationToken cancellationToken) Task ISocketOutput.WriteAsync(ArraySegment<byte> buffer, bool immediate, bool chunk, CancellationToken cancellationToken)
{ {
return WriteAsync(buffer, immediate, chunk); if (cancellationToken.IsCancellationRequested)
{
_connection?.Abort();
return TaskUtilities.GetCancelledTask(cancellationToken);
}
return WriteAsync(buffer, cancellationToken, immediate, chunk);
} }
private static void BytesBetween(MemoryPoolIterator2 start, MemoryPoolIterator2 end, out int bytes, out int buffers) private static void BytesBetween(MemoryPoolIterator2 start, MemoryPoolIterator2 end, out int bytes, out int buffers)
@ -649,6 +740,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {
public bool IsSync; public bool IsSync;
public int BytesToWrite; public int BytesToWrite;
public CancellationToken CancellationToken;
public IDisposable CancellationRegistration;
public TaskCompletionSource<object> CompletionSource; public TaskCompletionSource<object> CompletionSource;
} }
} }

View File

@ -29,6 +29,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
void ConnectionWriteCallback(long connectionId, int status); void ConnectionWriteCallback(long connectionId, int status);
void ConnectionError(long connectionId, Exception ex);
void ConnectionDisconnectedWrite(long connectionId, int count, Exception ex);
void ApplicationError(Exception ex); void ApplicationError(Exception ex);
} }
} }

View File

@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
public interface IThreadPool public interface IThreadPool
{ {
void Complete(TaskCompletionSource<object> tcs); void Complete(TaskCompletionSource<object> tcs);
void Cancel(TaskCompletionSource<object> tcs);
void Error(TaskCompletionSource<object> tcs, Exception ex); void Error(TaskCompletionSource<object> tcs, Exception ex);
void Run(Action action); void Run(Action action);
} }

View File

@ -21,6 +21,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel
private static readonly Action<ILogger, long, int, Exception> _connectionWroteFin; private static readonly Action<ILogger, long, int, Exception> _connectionWroteFin;
private static readonly Action<ILogger, long, Exception> _connectionKeepAlive; private static readonly Action<ILogger, long, Exception> _connectionKeepAlive;
private static readonly Action<ILogger, long, Exception> _connectionDisconnect; private static readonly Action<ILogger, long, Exception> _connectionDisconnect;
private static readonly Action<ILogger, long, Exception> _connectionError;
private static readonly Action<ILogger, long, int, Exception> _connectionDisconnectedWrite;
protected readonly ILogger _logger; protected readonly ILogger _logger;
@ -39,6 +41,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel
// ConnectionWrite: Reserved: 11 // ConnectionWrite: Reserved: 11
// ConnectionWriteCallback: Reserved: 12 // ConnectionWriteCallback: Reserved: 12
// ApplicationError: Reserved: 13 - LoggerMessage.Define overload not present // ApplicationError: Reserved: 13 - LoggerMessage.Define overload not present
_connectionError = LoggerMessage.Define<long>(LogLevel.Information, 14, @"Connection id ""{ConnectionId}"" communication error");
_connectionDisconnectedWrite = LoggerMessage.Define<long, int>(LogLevel.Debug, 15, @"Connection id ""{ConnectionId}"" write of ""{count}"" bytes to disconnected client.");
} }
public KestrelTrace(ILogger logger) public KestrelTrace(ILogger logger)
@ -114,6 +118,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel
_logger.LogError(13, "An unhandled exception was thrown by the application.", ex); _logger.LogError(13, "An unhandled exception was thrown by the application.", ex);
} }
public virtual void ConnectionError(long connectionId, Exception ex)
{
_connectionError(_logger, connectionId, ex);
}
public virtual void ConnectionDisconnectedWrite(long connectionId, int count, Exception ex)
{
_connectionDisconnectedWrite(_logger, connectionId, count, ex);
}
public virtual void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func<object, Exception, string> formatter) public virtual void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func<object, Exception, string> formatter)
{ {
_logger.Log(logLevel, eventId, state, exception, formatter); _logger.Log(logLevel, eventId, state, exception, formatter);

View File

@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
private readonly IKestrelTrace _log; private readonly IKestrelTrace _log;
private readonly WaitCallback _runAction; private readonly WaitCallback _runAction;
private readonly WaitCallback _cancelTcs;
private readonly WaitCallback _completeTcs; private readonly WaitCallback _completeTcs;
public LoggingThreadPool(IKestrelTrace log) public LoggingThreadPool(IKestrelTrace log)
@ -42,6 +43,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
_log.ApplicationError(e); _log.ApplicationError(e);
} }
}; };
_cancelTcs = (o) =>
{
try
{
((TaskCompletionSource<object>)o).TrySetCanceled();
}
catch (Exception e)
{
_log.ApplicationError(e);
}
};
} }
public void Run(Action action) public void Run(Action action)
@ -54,6 +67,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
ThreadPool.QueueUserWorkItem(_completeTcs, tcs); ThreadPool.QueueUserWorkItem(_completeTcs, tcs);
} }
public void Cancel(TaskCompletionSource<object> tcs)
{
ThreadPool.QueueUserWorkItem(_cancelTcs, tcs);
}
public void Error(TaskCompletionSource<object> tcs, Exception ex) public void Error(TaskCompletionSource<object> tcs, Exception ex)
{ {
// ex ang _log are closure captured // ex ang _log are closure captured

View File

@ -724,6 +724,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
public void CopyFrom(byte[] data, int offset, int count) public void CopyFrom(byte[] data, int offset, int count)
{ {
if (IsDefault)
{
return;
}
Debug.Assert(_block != null); Debug.Assert(_block != null);
Debug.Assert(_block.Next == null); Debug.Assert(_block.Next == null);
Debug.Assert(_block.End == _index); Debug.Assert(_block.End == _index);
@ -766,6 +771,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
public unsafe void CopyFromAscii(string data) public unsafe void CopyFromAscii(string data)
{ {
if (IsDefault)
{
return;
}
Debug.Assert(_block != null); Debug.Assert(_block != null);
Debug.Assert(_block.Next == null); Debug.Assert(_block.Next == null);
Debug.Assert(_block.End == _index); Debug.Assert(_block.End == _index);

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 System.Threading.Tasks;
namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
@ -13,5 +14,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
public static Task CompletedTask = Task.FromResult<object>(null); public static Task CompletedTask = Task.FromResult<object>(null);
#endif #endif
public static Task<int> ZeroTask = Task.FromResult(0); public static Task<int> ZeroTask = Task.FromResult(0);
public static Task GetCancelledTask(CancellationToken cancellationToken)
{
#if DOTNET5_4
return Task.FromCanceled(cancellationToken);
#else
var tcs = new TaskCompletionSource<object>();
tcs.TrySetCanceled();
return tcs.Task;
#endif
}
public static Task<int> GetCancelledZeroTask()
{
// Task<int>.FromCanceled doesn't return Task<int>
var tcs = new TaskCompletionSource<int>();
tcs.TrySetCanceled();
return tcs.Task;
}
} }
} }

View File

@ -1084,7 +1084,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
} }
await Assert.ThrowsAsync<IOException>(async () => await readTcs.Task); await Assert.ThrowsAsync<TaskCanceledException>(async () => await readTcs.Task);
// The cancellation token for only the last request should be triggered. // The cancellation token for only the last request should be triggered.
var abortedRequestId = await registrationTcs.Task; var abortedRequestId = await registrationTcs.Task;
@ -1096,6 +1096,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Test hangs after execution on Mono.")]
public async Task FailedWritesResultInAbortedRequest(ServiceContext testContext) public async Task FailedWritesResultInAbortedRequest(ServiceContext testContext)
{ {
const int resetEventTimeout = 2000;
// This should match _maxBytesPreCompleted in SocketOutput
const int maxBytesPreCompleted = 65536;
// Ensure string is long enough to disable write-behind buffering
var largeString = new string('a', maxBytesPreCompleted + 1);
var writeTcs = new TaskCompletionSource<object>(); var writeTcs = new TaskCompletionSource<object>();
var registrationWh = new ManualResetEventSlim(); var registrationWh = new ManualResetEventSlim();
var connectionCloseWh = new ManualResetEventSlim(); var connectionCloseWh = new ManualResetEventSlim();
@ -1119,7 +1125,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
// Ensure write is long enough to disable write-behind buffering // Ensure write is long enough to disable write-behind buffering
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
await response.WriteAsync(new string('a', 65537)); await response.WriteAsync(largeString).ConfigureAwait(false);
} }
} }
catch (Exception ex) catch (Exception ex)
@ -1127,7 +1133,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
writeTcs.SetException(ex); writeTcs.SetException(ex);
// Give a chance for RequestAborted to trip before the app completes // Give a chance for RequestAborted to trip before the app completes
registrationWh.Wait(1000); registrationWh.Wait(resetEventTimeout);
throw; throw;
} }
@ -1141,16 +1147,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"POST / HTTP/1.1", "POST / HTTP/1.1",
"Content-Length: 5", "Content-Length: 5",
"", "",
"Hello"); "Hello").ConfigureAwait(false);
// Don't wait to receive the response. Just close the socket. // Don't wait to receive the response. Just close the socket.
} }
connectionCloseWh.Set(); connectionCloseWh.Set();
// Write failed // Write failed
await Assert.ThrowsAsync<IOException>(async () => await writeTcs.Task); await Assert.ThrowsAsync<TaskCanceledException>(async () => await writeTcs.Task);
// RequestAborted tripped // RequestAborted tripped
Assert.True(registrationWh.Wait(200)); Assert.True(registrationWh.Wait(resetEventTimeout));
} }
} }

View File

@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
private static void TestConcurrentFaultedTask(Task t) private static void TestConcurrentFaultedTask(Task t)
{ {
Assert.True(t.IsFaulted); Assert.True(t.IsFaulted);
Assert.IsType(typeof(System.IO.IOException), t.Exception.InnerException); Assert.IsType(typeof(System.InvalidOperationException), t.Exception.InnerException);
Assert.Equal(t.Exception.InnerException.Message, "Concurrent reads are not supported."); Assert.Equal(t.Exception.InnerException.Message, "Concurrent reads are not supported.");
} }

View File

@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var completedWh = new ManualResetEventSlim(); var completedWh = new ManualResetEventSlim();
// Act // Act
socketOutput.WriteAsync(buffer).ContinueWith( socketOutput.WriteAsync(buffer, default(CancellationToken)).ContinueWith(
(t) => (t) =>
{ {
Assert.Null(t.Exception); Assert.Null(t.Exception);
@ -101,14 +101,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}; };
// Act // Act
socketOutput.WriteAsync(buffer).ContinueWith(onCompleted); socketOutput.WriteAsync(buffer, default(CancellationToken)).ContinueWith(onCompleted);
// Assert // Assert
// The first write should pre-complete since it is <= _maxBytesPreCompleted. // The first write should pre-complete since it is <= _maxBytesPreCompleted.
Assert.True(completedWh.Wait(1000)); Assert.True(completedWh.Wait(1000));
// Arrange // Arrange
completedWh.Reset(); completedWh.Reset();
// Act // Act
socketOutput.WriteAsync(buffer).ContinueWith(onCompleted); socketOutput.WriteAsync(buffer, default(CancellationToken)).ContinueWith(onCompleted);
// Assert // Assert
// Too many bytes are already pre-completed for the second write to pre-complete. // Too many bytes are already pre-completed for the second write to pre-complete.
Assert.False(completedWh.Wait(1000)); Assert.False(completedWh.Wait(1000));
@ -162,28 +162,28 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}; };
// Act // Act
socketOutput.WriteAsync(halfBuffer, false).ContinueWith(onCompleted); socketOutput.WriteAsync(halfBuffer, default(CancellationToken), false).ContinueWith(onCompleted);
// Assert // Assert
// The first write should pre-complete since it is not immediate. // The first write should pre-complete since it is not immediate.
Assert.True(completedWh.Wait(1000)); Assert.True(completedWh.Wait(1000));
// Arrange // Arrange
completedWh.Reset(); completedWh.Reset();
// Act // Act
socketOutput.WriteAsync(halfBuffer).ContinueWith(onCompleted); socketOutput.WriteAsync(halfBuffer, default(CancellationToken)).ContinueWith(onCompleted);
// Assert // Assert
// The second write should pre-complete since it is <= _maxBytesPreCompleted. // The second write should pre-complete since it is <= _maxBytesPreCompleted.
Assert.True(completedWh.Wait(1000)); Assert.True(completedWh.Wait(1000));
// Arrange // Arrange
completedWh.Reset(); completedWh.Reset();
// Act // Act
socketOutput.WriteAsync(halfBuffer, false).ContinueWith(onCompleted); socketOutput.WriteAsync(halfBuffer, default(CancellationToken), false).ContinueWith(onCompleted);
// Assert // Assert
// The third write should pre-complete since it is not immediate, even though too many. // The third write should pre-complete since it is not immediate, even though too many.
Assert.True(completedWh.Wait(1000)); Assert.True(completedWh.Wait(1000));
// Arrange // Arrange
completedWh.Reset(); completedWh.Reset();
// Act // Act
socketOutput.WriteAsync(halfBuffer).ContinueWith(onCompleted); socketOutput.WriteAsync(halfBuffer, default(CancellationToken)).ContinueWith(onCompleted);
// Assert // Assert
// Too many bytes are already pre-completed for the fourth write to pre-complete. // Too many bytes are already pre-completed for the fourth write to pre-complete.
Assert.False(completedWh.Wait(1000)); Assert.False(completedWh.Wait(1000));
@ -198,6 +198,116 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
} }
[Fact]
public async Task OnlyWritesRequestingCancellationAreErroredOnCancellation()
{
// This should match _maxBytesPreCompleted in SocketOutput
var maxBytesPreCompleted = 65536;
var completeQueue = new Queue<Action<int>>();
// Arrange
var mockLibuv = new MockLibuv
{
OnWrite = (socket, buffers, triggerCompleted) =>
{
completeQueue.Enqueue(triggerCompleted);
return 0;
}
};
using (var kestrelEngine = new KestrelEngine(mockLibuv, new TestServiceContext()))
using (var memory = new MemoryPool2())
{
kestrelEngine.Start(count: 1);
var kestrelThread = kestrelEngine.Threads[0];
var socket = new MockSocket(kestrelThread.Loop.ThreadId, new TestKestrelTrace());
var trace = new KestrelTrace(new TestKestrelTrace());
var ltp = new LoggingThreadPool(trace);
ISocketOutput socketOutput = new SocketOutput(kestrelThread, socket, memory, null, 0, trace, ltp, new Queue<UvWriteReq>());
var bufferSize = maxBytesPreCompleted;
var data = new byte[bufferSize];
var fullBuffer = new ArraySegment<byte>(data, 0, bufferSize);
var cts = new CancellationTokenSource();
// Act
var task1Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: cts.Token);
// task1 should complete sucessfully as < _maxBytesPreCompleted
// First task is completed and sucessful
Assert.True(task1Success.IsCompleted);
Assert.False(task1Success.IsCanceled);
Assert.False(task1Success.IsFaulted);
task1Success.GetAwaiter().GetResult();
// following tasks should wait.
var task2Throw = socketOutput.WriteAsync(fullBuffer, cancellationToken: cts.Token);
var task3Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: default(CancellationToken));
// Give time for tasks to perculate
await Task.Delay(2000).ConfigureAwait(false);
// Second task is not completed
Assert.False(task2Throw.IsCompleted);
Assert.False(task2Throw.IsCanceled);
Assert.False(task2Throw.IsFaulted);
// Third task is not completed
Assert.False(task3Success.IsCompleted);
Assert.False(task3Success.IsCanceled);
Assert.False(task3Success.IsFaulted);
cts.Cancel();
// Give time for tasks to perculate
await Task.Delay(2000).ConfigureAwait(false);
// Second task is now cancelled
Assert.True(task2Throw.IsCompleted);
Assert.True(task2Throw.IsCanceled);
Assert.False(task2Throw.IsFaulted);
// Third task is now completed
Assert.True(task3Success.IsCompleted);
Assert.False(task3Success.IsCanceled);
Assert.False(task3Success.IsFaulted);
// Fourth task immediately cancels as the token is cancelled
var task4Throw = socketOutput.WriteAsync(fullBuffer, cancellationToken: cts.Token);
Assert.True(task4Throw.IsCompleted);
Assert.True(task4Throw.IsCanceled);
Assert.False(task4Throw.IsFaulted);
Assert.Throws<TaskCanceledException>(() => task4Throw.GetAwaiter().GetResult());
var task5Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: default(CancellationToken));
// task5 should complete immedately
Assert.True(task5Success.IsCompleted);
Assert.False(task5Success.IsCanceled);
Assert.False(task5Success.IsFaulted);
cts = new CancellationTokenSource();
var task6Throw = socketOutput.WriteAsync(fullBuffer, cancellationToken: cts.Token);
// task6 should complete immedately but not cancel as its cancelation token isn't set
Assert.True(task6Throw.IsCompleted);
Assert.False(task6Throw.IsCanceled);
Assert.False(task6Throw.IsFaulted);
Assert.Throws<TaskCanceledException>(() => task6Throw.GetAwaiter().GetResult());
Assert.True(true);
}
}
[Fact] [Fact]
public void WritesDontGetCompletedTooQuickly() public void WritesDontGetCompletedTooQuickly()
{ {
@ -247,7 +357,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}; };
// Act (Pre-complete the maximum number of bytes in preparation for the rest of the test) // Act (Pre-complete the maximum number of bytes in preparation for the rest of the test)
socketOutput.WriteAsync(buffer).ContinueWith(onCompleted); socketOutput.WriteAsync(buffer, default(CancellationToken)).ContinueWith(onCompleted);
// Assert // Assert
// The first write should pre-complete since it is <= _maxBytesPreCompleted. // The first write should pre-complete since it is <= _maxBytesPreCompleted.
Assert.True(completedWh.Wait(1000)); Assert.True(completedWh.Wait(1000));
@ -257,8 +367,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
onWriteWh.Reset(); onWriteWh.Reset();
// Act // Act
socketOutput.WriteAsync(buffer).ContinueWith(onCompleted); socketOutput.WriteAsync(buffer, default(CancellationToken)).ContinueWith(onCompleted);
socketOutput.WriteAsync(buffer).ContinueWith(onCompleted2); socketOutput.WriteAsync(buffer, default(CancellationToken)).ContinueWith(onCompleted2);
Assert.True(onWriteWh.Wait(1000)); Assert.True(onWriteWh.Wait(1000));
completeQueue.Dequeue()(0); completeQueue.Dequeue()(0);
@ -320,7 +430,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
socketOutput.ProducingComplete(end); socketOutput.ProducingComplete(end);
// A call to Write is required to ensure a write is scheduled // A call to Write is required to ensure a write is scheduled
socketOutput.WriteAsync(default(ArraySegment<byte>)); socketOutput.WriteAsync(default(ArraySegment<byte>), default(CancellationToken));
Assert.True(nBufferWh.Wait(1000)); Assert.True(nBufferWh.Wait(1000));
Assert.Equal(2, nBuffers); Assert.Equal(2, nBuffers);