aspnetcore/src/Microsoft.AspNetCore.Server.../Internal/Http/SocketOutput.cs

301 lines
9.1 KiB
C#

// 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.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public class SocketOutput : ISocketOutput
{
private static readonly ArraySegment<byte> _emptyData = new ArraySegment<byte>(new byte[0]);
private readonly KestrelThread _thread;
private readonly UvStreamHandle _socket;
private readonly Connection _connection;
private readonly string _connectionId;
private readonly IKestrelTrace _log;
// This locks access to to all of the below fields
private readonly object _contextLock = new object();
private bool _cancelled = false;
private bool _completed = false;
private Exception _lastWriteError;
private readonly WriteReqPool _writeReqPool;
private readonly IPipe _pipe;
private Task _writingTask;
// https://github.com/dotnet/corefxlab/issues/1334
// Pipelines don't support multiple awaiters on flush
// this is temporary until it does
private TaskCompletionSource<object> _flushTcs;
private readonly object _flushLock = new object();
private readonly Action _onFlushCallback;
public SocketOutput(
IPipe pipe,
KestrelThread thread,
UvStreamHandle socket,
Connection connection,
string connectionId,
IKestrelTrace log)
{
_pipe = pipe;
// We need to have empty pipe at this moment so callback
// get's scheduled
_writingTask = StartWrites();
_thread = thread;
_socket = socket;
_connection = connection;
_connectionId = connectionId;
_log = log;
_writeReqPool = thread.WriteReqPool;
_onFlushCallback = OnFlush;
}
public async Task WriteAsync(
ArraySegment<byte> buffer,
CancellationToken cancellationToken,
bool chunk = false)
{
var writableBuffer = default(WritableBuffer);
lock (_contextLock)
{
if (_socket.IsClosed)
{
_log.ConnectionDisconnectedWrite(_connectionId, buffer.Count, _lastWriteError);
return;
}
if (_completed)
{
return;
}
writableBuffer = _pipe.Writer.Alloc();
if (buffer.Count > 0)
{
if (chunk)
{
ChunkWriter.WriteBeginChunkBytes(ref writableBuffer, buffer.Count);
}
writableBuffer.Write(buffer);
if (chunk)
{
ChunkWriter.WriteEndChunkBytes(ref writableBuffer);
}
}
writableBuffer.Commit();
}
await FlushAsync(writableBuffer);
}
public void End(ProduceEndType endType)
{
if (endType == ProduceEndType.SocketShutdown)
{
// Graceful shutdown
_pipe.Reader.CancelPendingRead();
}
lock (_contextLock)
{
_completed = true;
}
// We're done writing
_pipe.Writer.Complete();
}
private Task FlushAsync(WritableBuffer writableBuffer)
{
var awaitable = writableBuffer.FlushAsync();
if (awaitable.IsCompleted)
{
// The flush task can't fail today
return TaskCache.CompletedTask;
}
return FlushAsyncAwaited(awaitable);
}
private Task FlushAsyncAwaited(WritableBufferAwaitable awaitable)
{
// https://github.com/dotnet/corefxlab/issues/1334
// Since the flush awaitable doesn't currently support multiple awaiters
// we need to use a task to track the callbacks.
// All awaiters get the same task
lock (_flushLock)
{
if (_flushTcs == null || _flushTcs.Task.IsCompleted)
{
_flushTcs = new TaskCompletionSource<object>();
awaitable.OnCompleted(_onFlushCallback);
}
}
return _flushTcs.Task;
}
private void OnFlush()
{
_flushTcs.TrySetResult(null);
}
void ISocketOutput.Write(ArraySegment<byte> buffer, bool chunk)
{
WriteAsync(buffer, default(CancellationToken), chunk).GetAwaiter().GetResult();
}
Task ISocketOutput.WriteAsync(ArraySegment<byte> buffer, bool chunk, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
_connection.AbortAsync();
_cancelled = true;
return TaskUtilities.GetCancelledTask(cancellationToken);
}
else if (_cancelled)
{
return TaskCache.CompletedTask;
}
return WriteAsync(buffer, cancellationToken, chunk);
}
void ISocketOutput.Flush()
{
WriteAsync(_emptyData, default(CancellationToken)).GetAwaiter().GetResult();
}
Task ISocketOutput.FlushAsync(CancellationToken cancellationToken)
{
return WriteAsync(_emptyData, cancellationToken);
}
public void Write<T>(Action<WritableBuffer, T> callback, T state)
{
lock (_contextLock)
{
if (_completed)
{
return;
}
var buffer = _pipe.Writer.Alloc();
callback(buffer, state);
buffer.Commit();
}
}
public async Task StartWrites()
{
while (true)
{
var result = await _pipe.Reader.ReadAsync();
var buffer = result.Buffer;
try
{
if (!buffer.IsEmpty)
{
var writeReq = _writeReqPool.Allocate();
var writeResult = await writeReq.WriteAsync(_socket, buffer);
_writeReqPool.Return(writeReq);
// REVIEW: Locking here, do we need to take the context lock?
OnWriteCompleted(writeResult.Status, writeResult.Error);
}
if (result.IsCancelled)
{
// Send a FIN
await ShutdownAsync();
}
if (buffer.IsEmpty && result.IsCompleted)
{
break;
}
}
finally
{
_pipe.Reader.Advance(result.Buffer.End);
}
}
// We're done reading
_pipe.Reader.Complete();
_socket.Dispose();
_connection.OnSocketClosed();
_log.ConnectionStop(_connectionId);
}
private void OnWriteCompleted(int writeStatus, Exception writeError)
{
// 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();
_cancelled = true;
_lastWriteError = error;
}
if (error == null)
{
_log.ConnectionWriteCallback(_connectionId, status);
}
else
{
// Log connection resets at a lower (Debug) level.
if (status == Constants.ECONNRESET)
{
_log.ConnectionReset(_connectionId);
}
else
{
_log.ConnectionError(_connectionId, error);
}
}
}
private Task ShutdownAsync()
{
var tcs = new TaskCompletionSource<object>();
_log.ConnectionWriteFin(_connectionId);
var shutdownReq = new UvShutdownReq(_log);
shutdownReq.Init(_thread.Loop);
shutdownReq.Shutdown(_socket, (req, status, state) =>
{
req.Dispose();
_log.ConnectionWroteFin(_connectionId, status);
tcs.TrySetResult(null);
},
this);
return tcs.Task;
}
}
}