private async Task ProcessClientConnectionAsync(ClientConnectionContext connection) { try { // Writing from the application to the service var transport = ProcessOutgoingMessagesAsync(connection, connection.OutgoingAborted); // Waiting for the application to shutdown so we can clean up the connection var app = ProcessApplicationTaskAsyncCore(connection); var task = await Task.WhenAny(app, transport); // remove it from the connection list RemoveClientConnection(connection.ConnectionId); // This is the exception from application Exception exception = null; if (task == app) { exception = app.Exception?.GetBaseException(); // there is no need to write to the transport as application is no longer running Log.WaitingForTransport(Logger); // app task completes connection.Transport.Output, which will completes connection.Application.Input and ends the transport // Transports are written by us and are well behaved, wait for them to drain connection.CancelOutgoing(_closeTimeOutMilliseconds); // transport never throws await transport; } else { // transport task ends first, no data will be dispatched out Log.WaitingForApplication(Logger); try { // always wait for the application to complete await app; } catch (Exception e) { exception = e; } } if (exception != null) { Log.ApplicationTaskFailed(Logger, exception); } // If we aren't already aborted, we send the abort message to the service if (connection.AbortOnClose) { // Inform the Service that we will remove the client because SignalR told us it is disconnected. var serviceMessage = new CloseConnectionMessage(connection.ConnectionId, errorMessage: exception?.Message); // when it fails, it means the underlying connection is dropped // service is responsible for closing the client connections in this case and there is no need to throw await SafeWriteAsync(serviceMessage); Log.CloseConnection(Logger, connection.ConnectionId); } Log.ConnectedEnding(Logger, connection.ConnectionId); } catch (Exception e) { // When it throws, there must be something wrong Log.ProcessConnectionFailed(Logger, connection.ConnectionId, e); } finally { connection.OnCompleted(); } }
private async Task ProcessOutgoingMessagesAsync(ClientConnectionContext connection, CancellationToken token = default) { try { if (connection.IsMigrated) { using var timeoutToken = new CancellationTokenSource(DefaultHandshakeTimeout); using var source = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutToken.Token); // A handshake response is not expected to be given // if the connection was migrated from another server, // since the connection hasn't been `dropped` from the client point of view. if (!await SkipHandshakeResponse(connection, source.Token)) { return; } } while (true) { var result = await connection.Application.Input.ReadAsync(token); if (result.IsCanceled) { break; } var buffer = result.Buffer; if (!buffer.IsEmpty) { try { // Forward the message to the service await WriteAsync(new ConnectionDataMessage(connection.ConnectionId, buffer)); } catch (Exception ex) { Log.ErrorSendingMessage(Logger, ex); } } if (result.IsCompleted) { // This connection ended (the application itself shut down) we should remove it from the list of connections break; } connection.Application.Input.AdvanceTo(buffer.End); } } catch (Exception ex) { // The exception means application fail to process input anymore // Cancel any pending flush so that we can quit and perform disconnect // Here is abort close and WaitOnApplicationTask will send close message to notify client to disconnect Log.SendLoopStopped(Logger, connection.ConnectionId, ex); connection.Application.Output.CancelPendingFlush(); } finally { connection.Application.Input.Complete(); await PerformDisconnectAsyncCore(connection.ConnectionId, true); connection.OnCompleted(); } }