228 lines
8.7 KiB
C#
228 lines
8.7 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.Buffers.Text;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.IO.Pipelines;
|
|
using System.Net.WebSockets;
|
|
using System.Security.Cryptography;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.Extensions.Internal;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Microsoft.AspNetCore.Http.Connections.Internal
|
|
{
|
|
public partial class HttpConnectionManager
|
|
{
|
|
// TODO: Consider making this configurable? At least for testing?
|
|
private static readonly TimeSpan _heartbeatTickRate = TimeSpan.FromSeconds(1);
|
|
|
|
private static readonly RNGCryptoServiceProvider _keyGenerator = new RNGCryptoServiceProvider();
|
|
|
|
private readonly ConcurrentDictionary<string, (HttpConnectionContext Connection, ValueStopwatch Timer)> _connections =
|
|
new ConcurrentDictionary<string, (HttpConnectionContext Connection, ValueStopwatch Timer)>(StringComparer.Ordinal);
|
|
private readonly TimerAwaitable _nextHeartbeat;
|
|
private readonly ILogger<HttpConnectionManager> _logger;
|
|
private readonly ILogger<HttpConnectionContext> _connectionLogger;
|
|
|
|
public HttpConnectionManager(ILoggerFactory loggerFactory, IApplicationLifetime appLifetime)
|
|
{
|
|
_logger = loggerFactory.CreateLogger<HttpConnectionManager>();
|
|
_connectionLogger = loggerFactory.CreateLogger<HttpConnectionContext>();
|
|
appLifetime.ApplicationStarted.Register(() => Start());
|
|
appLifetime.ApplicationStopping.Register(() => CloseConnections());
|
|
_nextHeartbeat = new TimerAwaitable(_heartbeatTickRate, _heartbeatTickRate);
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
_nextHeartbeat.Start();
|
|
|
|
// Start the timer loop
|
|
_ = ExecuteTimerLoop();
|
|
}
|
|
|
|
public bool TryGetConnection(string id, out HttpConnectionContext connection)
|
|
{
|
|
connection = null;
|
|
|
|
if (_connections.TryGetValue(id, out var pair))
|
|
{
|
|
connection = pair.Connection;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public HttpConnectionContext CreateConnection()
|
|
{
|
|
return CreateConnection(PipeOptions.Default, PipeOptions.Default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a connection without Pipes setup to allow saving allocations until Pipes are needed.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public HttpConnectionContext CreateConnection(PipeOptions transportPipeOptions, PipeOptions appPipeOptions)
|
|
{
|
|
var id = MakeNewConnectionId();
|
|
|
|
Log.CreatedNewConnection(_logger, id);
|
|
var connectionTimer = HttpConnectionsEventSource.Log.ConnectionStart(id);
|
|
var connection = new HttpConnectionContext(id, _connectionLogger);
|
|
var pair = DuplexPipe.CreateConnectionPair(transportPipeOptions, appPipeOptions);
|
|
connection.Transport = pair.Application;
|
|
connection.Application = pair.Transport;
|
|
|
|
_connections.TryAdd(id, (connection, connectionTimer));
|
|
return connection;
|
|
}
|
|
|
|
public void RemoveConnection(string id)
|
|
{
|
|
if (_connections.TryRemove(id, out var pair))
|
|
{
|
|
// Remove the connection completely
|
|
HttpConnectionsEventSource.Log.ConnectionStop(id, pair.Timer);
|
|
Log.RemovedConnection(_logger, id);
|
|
}
|
|
}
|
|
|
|
private static string MakeNewConnectionId()
|
|
{
|
|
// TODO: Use Span when WebEncoders implements Span methods https://github.com/aspnet/Home/issues/2966
|
|
// 128 bit buffer / 8 bits per byte = 16 bytes
|
|
var buffer = new byte[16];
|
|
_keyGenerator.GetBytes(buffer);
|
|
// Generate the id with RNGCrypto because we want a cryptographically random id, which GUID is not
|
|
return WebEncoders.Base64UrlEncode(buffer);
|
|
}
|
|
|
|
private async Task ExecuteTimerLoop()
|
|
{
|
|
Log.HeartBeatStarted(_logger);
|
|
|
|
// Dispose the timer when all the code consuming callbacks has completed
|
|
using (_nextHeartbeat)
|
|
{
|
|
// The TimerAwaitable will return true until Stop is called
|
|
while (await _nextHeartbeat)
|
|
{
|
|
try
|
|
{
|
|
await ScanAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.ScanningConnectionsFailed(_logger, ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
Log.HeartBeatEnded(_logger);
|
|
}
|
|
|
|
public async Task ScanAsync()
|
|
{
|
|
// Time the scan so we know if it gets slower than 1sec
|
|
var timer = ValueStopwatch.StartNew();
|
|
HttpConnectionsEventSource.Log.ScanningConnections();
|
|
Log.ScanningConnections(_logger);
|
|
|
|
// Scan the registered connections looking for ones that have timed out
|
|
foreach (var c in _connections)
|
|
{
|
|
HttpConnectionStatus status;
|
|
DateTimeOffset lastSeenUtc;
|
|
var connection = c.Value.Connection;
|
|
|
|
await connection.StateLock.WaitAsync();
|
|
|
|
try
|
|
{
|
|
// Capture the connection state
|
|
status = connection.Status;
|
|
|
|
lastSeenUtc = connection.LastSeenUtc;
|
|
}
|
|
finally
|
|
{
|
|
connection.StateLock.Release();
|
|
}
|
|
|
|
// Once the decision has been made to dispose we don't check the status again
|
|
// But don't clean up connections while the debugger is attached.
|
|
if (!Debugger.IsAttached && status == HttpConnectionStatus.Inactive && (DateTimeOffset.UtcNow - lastSeenUtc).TotalSeconds > 5)
|
|
{
|
|
Log.ConnectionTimedOut(_logger, connection.ConnectionId);
|
|
HttpConnectionsEventSource.Log.ConnectionTimedOut(connection.ConnectionId);
|
|
|
|
// This is most likely a long polling connection. The transport here ends because
|
|
// a poll completed and has been inactive for > 5 seconds so we wait for the
|
|
// application to finish gracefully
|
|
_ = DisposeAndRemoveAsync(connection, closeGracefully: true);
|
|
}
|
|
else
|
|
{
|
|
// Tick the heartbeat, if the connection is still active
|
|
connection.TickHeartbeat();
|
|
}
|
|
}
|
|
|
|
var elapsed = timer.GetElapsedTime();
|
|
HttpConnectionsEventSource.Log.ScannedConnections(elapsed);
|
|
Log.ScannedConnections(_logger, elapsed);
|
|
}
|
|
|
|
public void CloseConnections()
|
|
{
|
|
// Stop firing the timer
|
|
_nextHeartbeat.Stop();
|
|
|
|
var tasks = new List<Task>();
|
|
|
|
// REVIEW: In the future we can consider a hybrid where we first try to wait for shutdown
|
|
// for a certain time frame then after some grace period we shutdown more aggressively
|
|
foreach (var c in _connections)
|
|
{
|
|
// We're shutting down so don't wait for closing the application
|
|
tasks.Add(DisposeAndRemoveAsync(c.Value.Connection, closeGracefully: false));
|
|
}
|
|
|
|
Task.WaitAll(tasks.ToArray(), TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
public async Task DisposeAndRemoveAsync(HttpConnectionContext connection, bool closeGracefully)
|
|
{
|
|
try
|
|
{
|
|
await connection.DisposeAsync(closeGracefully);
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
Log.ConnectionReset(_logger, connection.ConnectionId, ex);
|
|
}
|
|
catch (WebSocketException ex) when (ex.InnerException is IOException)
|
|
{
|
|
Log.ConnectionReset(_logger, connection.ConnectionId, ex);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.FailedDispose(_logger, connection.ConnectionId, ex);
|
|
}
|
|
finally
|
|
{
|
|
// Remove it from the list after disposal so that's it's easy to see
|
|
// connections that might be in a hung state via the connections list
|
|
RemoveConnection(connection.ConnectionId);
|
|
}
|
|
}
|
|
}
|
|
}
|