File: TlsEventPumpPool.cs
Web Access
Project: src\aspnetcore\src\Servers\Kestrel\Transport.DirectTls\src\Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.csproj (Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Net;
using System.Net.Security;
using System.Threading.Channels;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.Connection;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls;
 
/// <summary>
/// Pool of TLS event pumps. Each pump owns a set of connections and handles
/// all TLS I/O for those connections on a dedicated thread.
///
/// With EPOLLEXCLUSIVE, all pumps can accept connections directly, distributing
/// the accept and handshake load across all workers.
/// </summary>
internal sealed class TlsEventPumpPool : IDisposable
{
    private readonly TlsEventPump[] _pumps;
    private readonly ILoggerFactory _loggerFactory;
 
    public TlsEventPumpPool(int pumpCount, ILoggerFactory loggerFactory, TimeSpan? handshakeTimeout = null)
    {
        _loggerFactory = loggerFactory;
 
        // Default: 1 pump per CPU core
        pumpCount = pumpCount > 0 ? pumpCount : Environment.ProcessorCount;
 
        // No timeout by default (used by tests that construct the pool directly); the transport factory
        // always supplies the endpoint's configured HandshakeTimeout.
        var effectiveHandshakeTimeout = handshakeTimeout ?? Timeout.InfiniteTimeSpan;
 
        _pumps = new TlsEventPump[pumpCount];
        for (int i = 0; i < pumpCount; i++)
        {
            _pumps[i] = new TlsEventPump(loggerFactory.CreateLogger<TlsEventPump>(), i, effectiveHandshakeTimeout);
        }
    }
 
    /// <summary>
    /// Start all pumps with a listen socket. Each pump registers the listen socket
    /// with EPOLLEXCLUSIVE so that only one pump wakes per incoming connection.
    /// </summary>
    public void StartWithListenSocket(
        int listenFd,
        EndPoint listenEndPoint,
        TlsContext tlsContext,
        Func<ConnectionContext?, string?, (TlsContext Context, RemoteCertificateValidationCallback? ClientCertificateValidation)>? contextResolver,
        ChannelWriter<DirectTlsConnection> readyConnections,
        MemoryPool<byte> memoryPool,
        bool noDelay,
        long maxReadBufferSize,
        long maxWriteBufferSize,
        Action<Exception> onFatalError,
        Action<ConnectionContext, ReadOnlySequence<byte>>? clientHelloCallback = null,
        ConnectionTracker? connectionTracker = null)
    {
        foreach (var pump in _pumps)
        {
            pump.StartWithListenSocket(
                listenFd,
                listenEndPoint,
                tlsContext,
                contextResolver,
                readyConnections,
                memoryPool,
                _loggerFactory,
                noDelay,
                maxReadBufferSize,
                maxWriteBufferSize,
                onFatalError,
                clientHelloCallback,
                connectionTracker);
        }
    }
 
    /// <summary>
    /// Stop every pump from accepting new connections (de-register the listen fd from each pump's epoll
    /// set and clear its cached listen fd). Established connections keep being serviced. Call this before
    /// closing the listen socket. Idempotent.
    /// </summary>
    public void StopAccepting()
    {
        foreach (var pump in _pumps)
        {
            pump.StopAccepting();
        }
    }
 
    /// <summary>
    /// Stops every pump and asynchronously waits for their threads to exit. Signals all pumps first so their
    /// threads wind down concurrently, then awaits each. Returns <see langword="true"/> only if every pump
    /// thread has actually exited - meaning no pump can touch the shared TLS contexts or memory pool any more,
    /// so the listener may release them. Returns <see langword="false"/> if any pump thread is still running
    /// after its stop timeout (a blocking TLS callback), in which case the listener MUST leak those shared
    /// resources rather than free memory a live pump can still reach. Idempotent.
    /// </summary>
    public async Task<bool> StopAndConfirmExitAsync(CancellationToken cancellationToken)
    {
        // Two phases: signal every pump before awaiting any, so a slow pump doesn't hold back the stop signal
        // to the others. The awaits then overlap, bounding total shutdown by the slowest pump, not their sum.
        foreach (var pump in _pumps)
        {
            pump.SignalStop();
        }
 
        var stops = new Task<bool>[_pumps.Length];
        for (int i = 0; i < _pumps.Length; i++)
        {
            stops[i] = _pumps[i].StopAndJoinAsync(cancellationToken);
        }
 
        var results = await Task.WhenAll(stops).ConfigureAwait(false);
 
        var allExited = true;
        foreach (var exited in results)
        {
            allExited &= exited;
        }
 
        return allExited;
    }
 
    public void Dispose() => StopAndConfirmExitAsync(CancellationToken.None).GetAwaiter().GetResult();
}