< Summary

Information
Class: IceRpc.Server
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Server.cs
Tag: 2300_35243572715
Line coverage
91%
Covered lines: 511
Uncovered lines: 48
Coverable lines: 559
Total lines: 1021
Line coverage: 91.4%
Branch coverage
89%
Covered branches: 79
Total branches: 88
Branch coverage: 89.7%
Method coverage
98%
Covered methods: 50
Fully covered methods: 41
Total methods: 51
Method coverage: 98%
Full method coverage: 80.3%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)91.66%242493.87%
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
DisposeAsync()100%66100%
PerformDisposeAsync()100%4476%
Listen()75%4484.61%
ListenAsync()90%101098.27%
ConnectAsync()100%121290.56%
DisposeDetachedConnectionAsync()83.33%66100%
ShutdownWhenRequestedAsync()100%22100%
ShutdownAsync(...)75%4485.71%
PerformShutdownAsync()66.66%8664.1%
ToString()100%11100%
IsRetryableAcceptException(...)100%44100%
get_ServerAddress()100%11100%
AcceptAsync()100%1152.17%
DisposeAsync()100%11100%
.ctor(...)100%11100%
ConnectTransportConnectionAsync()100%11100%
CreateProtocolConnection(...)100%11100%
DisposeAsync()100%11100%
RefuseTransportConnectionAsync(...)100%210%
.ctor(...)100%11100%
get_ServerAddress()100%11100%
AcceptAsync()100%11100%
DisposeAsync()100%11100%
.ctor(...)100%11100%
ConnectTransportConnectionAsync()100%11100%
CreateProtocolConnection(...)100%11100%
DisposeAsync()100%11100%
RefuseTransportConnectionAsync()100%11100%
.ctor(...)100%11100%
get_ServerAddress()100%11100%
DisposeAsync()100%11100%
AcceptAsync()100%11100%
.ctor(...)100%11100%
ConnectTransportConnectionAsync(...)100%11100%
CreateProtocolConnection(...)100%11100%
DisposeAsync()100%22100%
RefuseTransportConnectionAsync(...)100%11100%
.ctor(...)100%11100%
get_ServerAddress()100%11100%
AcceptAsync()100%11100%
DisposeAsync()100%11100%
.ctor(...)100%11100%
ConnectTransportConnectionAsync(...)100%11100%
CreateProtocolConnection(...)100%11100%
DisposeAsync()100%22100%
RefuseTransportConnectionAsync(...)50%22100%
.ctor(...)100%11100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Server.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Internal;
 4using IceRpc.Transports;
 5using IceRpc.Transports.Internal;
 6using Microsoft.Extensions.Logging;
 7using Microsoft.Extensions.Logging.Abstractions;
 8using System.Diagnostics;
 9using System.Net;
 10using System.Net.Security;
 11using System.Security.Authentication;
 12
 13namespace IceRpc;
 14
 15/// <summary>A server accepts connections from clients and dispatches the requests it receives over these connections.
 16/// </summary>
 17public sealed class Server : IAsyncDisposable
 18{
 9419    private readonly LinkedList<IProtocolConnection> _connections = new();
 20
 21    private readonly TimeSpan _connectTimeout;
 22
 23    // A detached connection is a protocol connection that we've decided to connect, or that is connecting, shutting
 24    // down or being disposed. It counts towards _maxConnections and both Server.ShutdownAsync and DisposeAsync wait for
 25    // detached connections to reach 0 using _detachedConnectionsTcs. Such a connection is "detached" because it's not
 26    // in _connections.
 27    private int _detachedConnectionCount;
 28
 9429    private readonly TaskCompletionSource _detachedConnectionsTcs =
 9430        new(TaskCreationOptions.RunContinuationsAsynchronously);
 31
 32    // A cancellation token source that is canceled by DisposeAsync.
 9433    private readonly CancellationTokenSource _disposedCts = new();
 34
 35    private Task? _disposeTask;
 36
 37    private readonly Func<IConnectorListener> _listenerFactory;
 38
 39    private Task? _listenTask;
 40
 41    private readonly int _maxConnections;
 42
 43    private readonly int _maxPendingConnections;
 44
 9445    private readonly Lock _mutex = new();
 46
 47    private readonly ServerAddress _serverAddress;
 48
 49    // A cancellation token source canceled by ShutdownAsync and DisposeAsync.
 50    private readonly CancellationTokenSource _shutdownCts;
 51
 52    private Task? _shutdownTask;
 53
 54    private readonly TimeSpan _shutdownTimeout;
 55
 56    /// <summary>Constructs a server.</summary>
 57    /// <param name="options">The server options.</param>
 58    /// <param name="duplexServerTransport">The transport used to create ice protocol connections. The <see
 59    /// langword="null" /> value is equivalent to <see cref="IDuplexServerTransport.Default" />.</param>
 60    /// <param name="multiplexedServerTransport">The transport used to create icerpc protocol connections. The <see
 61    /// langword="null" /> value is equivalent to <see cref="IMultiplexedServerTransport.Default" />.</param>
 62    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance"
 63    /// />.</param>
 9464    public Server(
 9465        ServerOptions options,
 9466        IDuplexServerTransport? duplexServerTransport = null,
 9467        IMultiplexedServerTransport? multiplexedServerTransport = null,
 9468        ILogger? logger = null)
 9469    {
 9470        if (options.ConnectionOptions.Dispatcher is null)
 071        {
 072            throw new ArgumentException($"{nameof(ServerOptions.ConnectionOptions.Dispatcher)} cannot be null");
 73        }
 74
 9475        logger ??= NullLogger.Instance;
 76
 9477        _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(_disposedCts.Token);
 78
 9479        duplexServerTransport ??= IDuplexServerTransport.Default;
 9480        multiplexedServerTransport ??= IMultiplexedServerTransport.Default;
 9481        _maxConnections = options.MaxConnections;
 9482        _maxPendingConnections = options.MaxPendingConnections;
 83
 9484        _connectTimeout = options.ConnectTimeout;
 9485        _shutdownTimeout = options.ShutdownTimeout;
 86
 9487        _serverAddress = options.ServerAddress;
 9488        if (_serverAddress.Transport is null)
 8389        {
 8390            _serverAddress = _serverAddress with
 8391            {
 8392                Transport = _serverAddress.Protocol == Protocol.Ice ?
 8393                    duplexServerTransport.DefaultName : multiplexedServerTransport.DefaultName
 8394            };
 8395        }
 96
 9497        if (options.ServerAuthenticationOptions?.ApplicationProtocols is not null)
 098        {
 099            throw new ArgumentException(
 0100                "The ApplicationProtocols property of the SSL server authentication options must be null. The ALPN is se
 0101                nameof(options));
 102        }
 103
 94104        var transportAddress = new TransportAddress
 94105        {
 94106            Host = _serverAddress.Host,
 94107            Port = _serverAddress.Port,
 94108            TransportName = _serverAddress.Transport,
 94109            Params = _serverAddress.Params
 94110        };
 111
 94112        _listenerFactory = () =>
 93113        {
 94114            IConnectorListener listener;
 94115
 93116            SslServerAuthenticationOptions? serverAuthenticationOptions = options.ServerAuthenticationOptions;
 93117            if (serverAuthenticationOptions is not null)
 5118            {
 5119                serverAuthenticationOptions = serverAuthenticationOptions.ShallowClone();
 5120                serverAuthenticationOptions.ApplicationProtocols = [_serverAddress.Protocol.AlpnProtocol];
 5121            }
 94122
 93123            if (_serverAddress.Protocol == Protocol.Ice)
 25124            {
 25125                IListener<IDuplexConnection> transportListener = duplexServerTransport.Listen(
 25126                    transportAddress,
 25127                    new DuplexConnectionOptions
 25128                    {
 25129                        MinSegmentSize = options.ConnectionOptions.MinSegmentSize,
 25130                        Pool = options.ConnectionOptions.Pool,
 25131                    },
 25132                    serverAuthenticationOptions);
 94133
 25134                listener = new IceConnectorListener(transportListener, _serverAddress, options.ConnectionOptions);
 25135            }
 94136            else
 68137            {
 68138                IListener<IMultiplexedConnection> transportListener = multiplexedServerTransport.Listen(
 68139                    transportAddress,
 68140                    new MultiplexedConnectionOptions
 68141                    {
 68142                        HandshakeTimeout = options.ConnectTimeout,
 68143                        MaxBidirectionalStreams = options.ConnectionOptions.MaxIceRpcBidirectionalStreams,
 68144                        // Add an additional stream for the icerpc protocol control stream.
 68145                        MaxUnidirectionalStreams =
 68146                            int.Min(options.ConnectionOptions.MaxIceRpcUnidirectionalStreams + 1, ushort.MaxValue),
 68147                        MinSegmentSize = options.ConnectionOptions.MinSegmentSize,
 68148                        Pool = options.ConnectionOptions.Pool
 68149                    },
 68150                    serverAuthenticationOptions);
 94151
 68152                listener = new IceRpcConnectorListener(
 68153                    transportListener,
 68154                    _serverAddress,
 68155                    options.ConnectionOptions,
 68156                    logger == NullLogger.Instance ? null : new LogTaskExceptionObserver(logger));
 68157            }
 94158
 93159            listener = new MetricsConnectorListenerDecorator(listener);
 93160            if (logger != NullLogger.Instance)
 10161            {
 10162                listener = new LogConnectorListenerDecorator(listener, logger);
 10163            }
 93164            return listener;
 187165        };
 94166    }
 167
 168    /// <summary>Constructs a server with the specified dispatcher and authentication options. All other properties
 169    /// use the <see cref="ServerOptions" /> defaults.</summary>
 170    /// <param name="dispatcher">The dispatcher of the server.</param>
 171    /// <param name="serverAuthenticationOptions">The SSL server authentication options. When not <see langword="null"
 172    /// />, the server will accept only secure connections.</param>
 173    /// <param name="duplexServerTransport">The transport used to create ice protocol connections. <see langword="null"
 174    /// /> is equivalent to <see cref="IDuplexServerTransport.Default" />.</param>
 175    /// <param name="multiplexedServerTransport">The transport used to create icerpc protocol connections. <see
 176    /// langword="null" /> is equivalent to <see cref="IMultiplexedServerTransport.Default" />.</param>
 177    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance"
 178    /// />.</param>
 179    public Server(
 180        IDispatcher dispatcher,
 181        SslServerAuthenticationOptions? serverAuthenticationOptions = null,
 182        IDuplexServerTransport? duplexServerTransport = null,
 183        IMultiplexedServerTransport? multiplexedServerTransport = null,
 184        ILogger? logger = null)
 1185        : this(
 1186            new ServerOptions
 1187            {
 1188                ServerAuthenticationOptions = serverAuthenticationOptions,
 1189                ConnectionOptions = new()
 1190                {
 1191                    Dispatcher = dispatcher,
 1192                }
 1193            },
 1194            duplexServerTransport,
 1195            multiplexedServerTransport,
 1196            logger)
 1197    {
 1198    }
 199
 200    /// <summary>Constructs a server with the specified dispatcher, server address and authentication options. All
 201    /// other properties use the <see cref="ServerOptions" /> defaults.</summary>
 202    /// <param name="dispatcher">The dispatcher of the server.</param>
 203    /// <param name="serverAddress">The server address of the server.</param>
 204    /// <param name="serverAuthenticationOptions">The SSL server authentication options. When not <see langword="null"
 205    /// />, the server will accept only secure connections.</param>
 206    /// <param name="duplexServerTransport">The transport used to create ice protocol connections. <see langword="null"
 207    /// /> is equivalent to <see cref="IDuplexServerTransport.Default" />.</param>
 208    /// <param name="multiplexedServerTransport">The transport used to create icerpc protocol connections. <see
 209    /// langword="null" /> is equivalent to <see cref="IMultiplexedServerTransport.Default" />.</param>
 210    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance"
 211    /// />.</param>
 212    public Server(
 213        IDispatcher dispatcher,
 214        ServerAddress serverAddress,
 215        SslServerAuthenticationOptions? serverAuthenticationOptions = null,
 216        IDuplexServerTransport? duplexServerTransport = null,
 217        IMultiplexedServerTransport? multiplexedServerTransport = null,
 218        ILogger? logger = null)
 34219        : this(
 34220            new ServerOptions
 34221            {
 34222                ServerAuthenticationOptions = serverAuthenticationOptions,
 34223                ConnectionOptions = new()
 34224                {
 34225                    Dispatcher = dispatcher,
 34226                },
 34227                ServerAddress = serverAddress
 34228            },
 34229            duplexServerTransport,
 34230            multiplexedServerTransport,
 34231            logger)
 34232    {
 34233    }
 234
 235    /// <summary>Constructs a server with the specified dispatcher, server address URI and authentication options. All
 236    /// other properties use the <see cref="ServerOptions" /> defaults.</summary>
 237    /// <param name="dispatcher">The dispatcher of the server.</param>
 238    /// <param name="serverAddressUri">A URI that represents the server address of the server.</param>
 239    /// <param name="serverAuthenticationOptions">The SSL server authentication options. When not <see langword="null"
 240    /// />, the server will accept only secure connections.</param>
 241    /// <param name="duplexServerTransport">The transport used to create ice protocol connections. <see langword="null"
 242    /// /> is equivalent to <see cref="IDuplexServerTransport.Default" />.</param>
 243    /// <param name="multiplexedServerTransport">The transport used to create icerpc protocol connections. <see
 244    /// langword="null" /> is equivalent to <see cref="IMultiplexedServerTransport.Default" />.</param>
 245    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance"
 246    /// />.</param>
 247    public Server(
 248        IDispatcher dispatcher,
 249        Uri serverAddressUri,
 250        SslServerAuthenticationOptions? serverAuthenticationOptions = null,
 251        IDuplexServerTransport? duplexServerTransport = null,
 252        IMultiplexedServerTransport? multiplexedServerTransport = null,
 253        ILogger? logger = null)
 14254        : this(
 14255            dispatcher,
 14256            new ServerAddress(serverAddressUri),
 14257            serverAuthenticationOptions,
 14258            duplexServerTransport,
 14259            multiplexedServerTransport,
 14260            logger)
 14261    {
 14262    }
 263
 264    /// <summary>Releases all resources allocated by this server. The server stops listening for new connections and
 265    /// disposes the connections it accepted from clients.</summary>
 266    /// <returns>A value task that completes when the disposal of all connections accepted by the server has completed.
 267    /// This includes connections that were active when this method is called and connections whose disposal was
 268    /// initiated prior to this call.</returns>
 269    /// <remarks>The disposal of an underlying connection of the server aborts invocations, cancels dispatches and
 270    /// disposes the underlying transport connection without waiting for the peer. To wait for invocations and
 271    /// dispatches to complete, call <see cref="ShutdownAsync" /> first. If the configured dispatcher does not complete
 272    /// promptly when its cancellation token is canceled, the disposal can hang.</remarks>
 273    public ValueTask DisposeAsync()
 95274    {
 275        lock (_mutex)
 95276        {
 95277            if (_disposeTask is null)
 94278            {
 94279                _shutdownTask ??= Task.CompletedTask;
 94280                if (_detachedConnectionCount == 0)
 86281                {
 86282                    _ = _detachedConnectionsTcs.TrySetResult();
 86283                }
 284
 94285                _disposeTask = PerformDisposeAsync();
 94286            }
 95287            return new(_disposeTask);
 288        }
 289
 290        async Task PerformDisposeAsync()
 94291        {
 94292            await Task.Yield(); // exit mutex lock
 293
 94294            _disposedCts.Cancel();
 295
 296            // _listenTask etc are immutable when _disposeTask is not null.
 297
 298            // Wait for shutdown before disposing connections.
 299            try
 94300            {
 94301                await _shutdownTask.ConfigureAwait(false);
 94302            }
 0303            catch
 0304            {
 305                // Ignore exceptions.
 0306            }
 307
 94308            if (_listenTask is not null)
 93309            {
 310                try
 93311                {
 93312                    await _listenTask.ConfigureAwait(false);
 93313                }
 0314                catch
 0315                {
 316                    // Ignore exceptions.
 0317                }
 318
 93319                await Task.WhenAll(
 93320                    _connections
 48321                        .Select(connection => connection.DisposeAsync().AsTask())
 93322                        .Append(_detachedConnectionsTcs.Task)).ConfigureAwait(false);
 93323            }
 324
 94325            _disposedCts.Dispose();
 94326            _shutdownCts.Dispose();
 94327        }
 95328    }
 329
 330    /// <summary>Starts accepting connections on the configured server address. Requests received over these connections
 331    /// are then dispatched by the configured dispatcher.</summary>
 332    /// <returns>The server address this server is listening on and that a client would connect to. This address is the
 333    /// same as the <see cref="ServerOptions.ServerAddress" /> of <see cref="ServerOptions" /> except its
 334    /// <see cref="ServerAddress.Transport" /> property is always non-null and its port number is never 0 when the host
 335    /// is an IP address.</returns>
 336    /// <exception cref="IceRpcException">Thrown when the server transport fails to listen on the configured <see
 337    /// cref="ServerOptions.ServerAddress" />.</exception>
 338    /// <exception cref="InvalidOperationException">Thrown when the server is already listening, shut down or shutting
 339    /// down.</exception>
 340    /// <exception cref="ObjectDisposedException">Throw when the server is disposed.</exception>
 341    /// <remarks><see cref="Listen" /> can also throw exceptions from the transport; for example, the transport can
 342    /// reject the server address.</remarks>
 343    public ServerAddress Listen()
 95344    {
 345        lock (_mutex)
 95346        {
 95347            ObjectDisposedException.ThrowIf(_disposeTask is not null, this);
 348
 94349            if (_shutdownTask is not null)
 0350            {
 0351                throw new InvalidOperationException($"Server '{this}' is shut down or shutting down.");
 352            }
 94353            if (_listenTask is not null)
 1354            {
 1355                throw new InvalidOperationException($"Server '{this}' is already listening.");
 356            }
 357
 93358            IConnectorListener listener = _listenerFactory();
 93359            _listenTask = ListenAsync(listener); // _listenTask owns listener and must dispose it
 93360            return listener.ServerAddress;
 361        }
 362
 363        async Task ListenAsync(IConnectorListener listener)
 93364        {
 93365            await Task.Yield(); // exit mutex lock
 366
 367            try
 93368            {
 93369                using var pendingConnectionSemaphore = new SemaphoreSlim(
 93370                    _maxPendingConnections,
 93371                    _maxPendingConnections);
 372
 185373                while (!_shutdownCts.IsCancellationRequested)
 185374                {
 185375                    await pendingConnectionSemaphore.WaitAsync(_shutdownCts.Token).ConfigureAwait(false);
 376
 184377                    IConnector? connector = null;
 378                    do
 303379                    {
 380                        try
 303381                        {
 303382                            (connector, _) = await listener.AcceptAsync(_shutdownCts.Token).ConfigureAwait(false);
 92383                        }
 211384                        catch (Exception exception) when (IsRetryableAcceptException(exception))
 119385                        {
 386                            // continue
 119387                        }
 211388                    }
 211389                    while (connector is null);
 390
 391                    // We don't wait for the connection to be activated or shutdown. This could take a while for some
 392                    // transports such as TLS based transports where the handshake requires few round trips between the
 393                    // client and server. Waiting could also cause a security issue if the client doesn't respond to the
 394                    // connection initialization as we wouldn't be able to accept new connections in the meantime. The
 395                    // call will eventually timeout if the ConnectTimeout expires.
 92396                    CancellationToken cancellationToken = _disposedCts.Token;
 92397                    _ = Task.Run(
 92398                        async () =>
 92399                        {
 92400                            try
 92401                            {
 92402                                await ConnectAsync(connector, cancellationToken).ConfigureAwait(false);
 87403                            }
 5404                            catch
 5405                            {
 92406                                // Ignore connection establishment failure. This failures are logged by the
 92407                                // LogConnectorDecorator
 5408                            }
 92409                            finally
 92410                            {
 92411                                // The connection dispose will dispose the transport connection if it has not been
 92412                                // adopted by the protocol connection.
 92413                                await connector.DisposeAsync().ConfigureAwait(false);
 92414
 92415                                // The pending connection semaphore is disposed by the listen task completion once
 92416                                // shutdown / dispose is initiated.
 92417                                lock (_mutex)
 92418                                {
 92419                                    if (_shutdownTask is null)
 88420                                    {
 88421                                        pendingConnectionSemaphore.Release();
 88422                                    }
 92423                                }
 92424                            }
 92425                        },
 92426                        CancellationToken.None); // the task must run to dispose the connector.
 92427                }
 0428            }
 93429            catch
 93430            {
 431                // Ignore. Exceptions thrown by listener.AcceptAsync are logged by the log decorator when appropriate.
 93432            }
 433            finally
 93434            {
 93435                await listener.DisposeAsync().ConfigureAwait(false);
 93436            }
 437
 438            async Task ConnectAsync(IConnector connector, CancellationToken cancellationToken)
 92439            {
 92440                using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 92441                connectCts.CancelAfter(_connectTimeout);
 442
 443                // Connect the transport connection first. This connection establishment can be interrupted by the
 444                // connect timeout or the server ShutdownAsync/DisposeAsync.
 92445                TransportConnectionInformation transportConnectionInformation =
 92446                    await connector.ConnectTransportConnectionAsync(connectCts.Token).ConfigureAwait(false);
 447
 87448                IProtocolConnection? protocolConnection = null;
 87449                bool serverBusy = false;
 450
 451                lock (_mutex)
 87452                {
 87453                    Debug.Assert(
 87454                        _maxConnections == 0 || _connections.Count + _detachedConnectionCount <= _maxConnections);
 455
 87456                    if (_shutdownTask is null)
 87457                    {
 87458                        if (_maxConnections > 0 && (_connections.Count + _detachedConnectionCount) == _maxConnections)
 7459                        {
 7460                            serverBusy = true;
 7461                        }
 462                        else
 80463                        {
 464                            // The protocol connection adopts the transport connection from the connector and it's
 465                            // now responsible for disposing of it.
 80466                            protocolConnection = connector.CreateProtocolConnection(transportConnectionInformation);
 80467                            _detachedConnectionCount++;
 80468                        }
 87469                    }
 87470                }
 471
 87472                if (protocolConnection is null)
 7473                {
 474                    try
 7475                    {
 7476                        await connector.RefuseTransportConnectionAsync(serverBusy, connectCts.Token)
 7477                            .ConfigureAwait(false);
 5478                    }
 2479                    catch
 2480                    {
 481                        // ignore and continue
 2482                    }
 483                    // The transport connection is disposed by the disposal of the connector.
 7484                }
 485                else
 80486                {
 487                    Task shutdownRequested;
 488                    try
 80489                    {
 80490                        (_, shutdownRequested) = await protocolConnection.ConnectAsync(connectCts.Token)
 80491                            .ConfigureAwait(false);
 80492                    }
 0493                    catch
 0494                    {
 0495                        await DisposeDetachedConnectionAsync(protocolConnection, withShutdown: false)
 0496                            .ConfigureAwait(false);
 0497                        throw;
 498                    }
 499
 80500                    LinkedListNode<IProtocolConnection>? listNode = null;
 501
 502                    lock (_mutex)
 503                    {
 80504                        if (_shutdownTask is null)
 505                        {
 79506                            listNode = _connections.AddLast(protocolConnection);
 507
 508                            // protocolConnection is no longer a detached connection since it's now "attached" in
 509                            // _connections.
 79510                            _detachedConnectionCount--;
 511                        }
 80512                    }
 513
 80514                    if (listNode is null)
 515                    {
 1516                        await DisposeDetachedConnectionAsync(protocolConnection, withShutdown: true)
 1517                            .ConfigureAwait(false);
 518                    }
 519                    else
 79520                    {
 521                        // Schedule removal after successful ConnectAsync.
 79522                        _ = ShutdownWhenRequestedAsync(protocolConnection, shutdownRequested, listNode);
 523                    }
 80524                }
 525            }
 526        }
 527
 528        async Task DisposeDetachedConnectionAsync(IProtocolConnection connection, bool withShutdown)
 32529        {
 32530            if (withShutdown)
 32531            {
 532                // _disposedCts is not disposed since we own a _backgroundConnectionDisposeCount.
 32533                using var cts = CancellationTokenSource.CreateLinkedTokenSource(_disposedCts.Token);
 32534                cts.CancelAfter(_shutdownTimeout);
 535
 536                try
 32537                {
 538                    // Can be canceled by DisposeAsync or the shutdown timeout.
 32539                    await connection.ShutdownAsync(cts.Token).ConfigureAwait(false);
 21540                }
 11541                catch
 11542                {
 543                    // Ignore connection shutdown failures. connection.ShutdownAsync makes sure it's an "expected"
 544                    // exception.
 11545                }
 32546            }
 547
 32548            await connection.DisposeAsync().ConfigureAwait(false);
 549            lock (_mutex)
 32550            {
 32551                if (--_detachedConnectionCount == 0 && _shutdownTask is not null)
 12552                {
 12553                    _detachedConnectionsTcs.SetResult();
 12554                }
 32555            }
 32556        }
 557
 558        // Remove the connection from _connections after a successful ConnectAsync.
 559        async Task ShutdownWhenRequestedAsync(
 560            IProtocolConnection connection,
 561            Task shutdownRequested,
 562            LinkedListNode<IProtocolConnection> listNode)
 79563        {
 79564            await shutdownRequested.ConfigureAwait(false);
 565
 566            lock (_mutex)
 65567            {
 65568                if (_shutdownTask is null)
 31569                {
 31570                    _connections.Remove(listNode);
 31571                    _detachedConnectionCount++;
 31572                }
 573                else
 34574                {
 575                    // _connections is immutable and ShutdownAsync/DisposeAsync is responsible to shut down/dispose
 576                    // this connection.
 34577                    return;
 578                }
 31579            }
 580
 31581            await DisposeDetachedConnectionAsync(connection, withShutdown: true).ConfigureAwait(false);
 65582        }
 273583    }
 584
 585    /// <summary>Gracefully shuts down this server: the server stops accepting new connections and shuts down gracefully
 586    /// all its connections.</summary>
 587    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 588    /// <returns>A task that completes successfully once the shutdown of all connections accepted by the server has
 589    /// completed. This includes connections that were active when this method is called and connections whose shutdown
 590    /// was initiated prior to this call.</returns>
 591    /// <exception cref="InvalidOperationException">Thrown when this method is called more than once.</exception>
 592    /// <exception cref="ObjectDisposedException">Thrown when the server is disposed.</exception>
 593    /// <remarks><para>The returned task can also complete with one of the following exceptions:</para>
 594    /// <list type="bullet">
 595    /// <item><description><see cref="IceRpcException" /> with error <see cref="IceRpcError.OperationAborted" /> if the
 596    /// server is disposed while being shut down.</description></item>
 597    /// <item><description><see cref="OperationCanceledException" /> if cancellation was requested through the
 598    /// cancellation token.</description></item>
 599    /// <item><description><see cref="TimeoutException" /> if the shutdown timed out.</description></item>
 600    /// </list>
 601    /// </remarks>
 602    public Task ShutdownAsync(CancellationToken cancellationToken = default)
 34603    {
 604        lock (_mutex)
 34605        {
 34606            ObjectDisposedException.ThrowIf(_disposeTask is not null, this);
 607
 34608            if (_shutdownTask is not null)
 0609            {
 0610                throw new InvalidOperationException($"Server '{this}' is shut down or shutting down.");
 611            }
 612
 34613            if (_detachedConnectionCount == 0)
 30614            {
 30615                _detachedConnectionsTcs.SetResult();
 30616            }
 617
 34618            _shutdownTask = PerformShutdownAsync();
 34619        }
 34620        return _shutdownTask;
 621
 622        async Task PerformShutdownAsync()
 34623        {
 34624            await Task.Yield(); // exit mutex lock
 625
 34626            _shutdownCts.Cancel();
 627
 628            // _listenTask is immutable once _shutdownTask is not null.
 34629            if (_listenTask is not null)
 34630            {
 631                try
 34632                {
 34633                    using var cts = CancellationTokenSource.CreateLinkedTokenSource(
 34634                        cancellationToken,
 34635                        _disposedCts.Token);
 636
 34637                    cts.CancelAfter(_shutdownTimeout);
 638
 639                    try
 34640                    {
 34641                        await Task.WhenAll(
 34642                            _connections
 14643                                .Select(connection => connection.ShutdownAsync(cts.Token))
 34644                                .Append(_listenTask.WaitAsync(cts.Token))
 34645                                .Append(_detachedConnectionsTcs.Task.WaitAsync(cts.Token)))
 34646                            .ConfigureAwait(false);
 33647                    }
 0648                    catch (OperationCanceledException)
 0649                    {
 0650                        throw;
 651                    }
 1652                    catch
 1653                    {
 654                        // Ignore _listenTask and connection shutdown exceptions
 655
 656                        // Throw OperationCanceledException if this WhenAll exception is hiding an OCE.
 1657                        cts.Token.ThrowIfCancellationRequested();
 1658                    }
 34659                }
 0660                catch (OperationCanceledException)
 0661                {
 0662                    cancellationToken.ThrowIfCancellationRequested();
 663
 0664                    if (_disposedCts.IsCancellationRequested)
 0665                    {
 0666                        throw new IceRpcException(
 0667                            IceRpcError.OperationAborted,
 0668                            "The shutdown was aborted because the server was disposed.");
 669                    }
 670                    else
 0671                    {
 0672                        throw new TimeoutException(
 0673                            $"The server shut down timed out after {_shutdownTimeout.TotalSeconds} s.");
 674                    }
 675                }
 34676            }
 34677        }
 34678    }
 679
 680    /// <summary>Returns a string that represents this server.</summary>
 681    /// <returns>A string that represents this server.</returns>
 1682    public override string ToString() => _serverAddress.ToString();
 683
 684    /// <summary>Returns true if the <see cref="IConnectorListener.AcceptAsync" /> failure can be retried.</summary>
 685    private static bool IsRetryableAcceptException(Exception exception) =>
 686        // Transports such as QUIC do the SSL handshake when the connection is accepted, this can throw
 687        // AuthenticationException if it fails.
 211688        exception is IceRpcException or AuthenticationException;
 689
 690    /// <summary>Provides a decorator that adds logging to a <see cref="IConnectorListener" />.</summary>
 691    private class LogConnectorListenerDecorator : IConnectorListener
 692    {
 46693        public ServerAddress ServerAddress => _decoratee.ServerAddress;
 694
 695        private readonly IConnectorListener _decoratee;
 696        private readonly ILogger _logger;
 697
 698        public async Task<(IConnector, EndPoint)> AcceptAsync(CancellationToken cancellationToken)
 18699        {
 700            try
 18701            {
 18702                (IConnector connector, EndPoint remoteNetworkAddress) =
 18703                    await _decoratee.AcceptAsync(cancellationToken).ConfigureAwait(false);
 704
 8705                _logger.LogConnectionAccepted(ServerAddress, remoteNetworkAddress);
 8706                return (
 8707                    new LogConnectorDecorator(connector, ServerAddress, remoteNetworkAddress, _logger),
 8708                    remoteNetworkAddress);
 709            }
 10710            catch (OperationCanceledException exception) when (exception.CancellationToken == cancellationToken)
 10711            {
 712                // Do not log this exception. The AcceptAsync call can fail with OperationCanceledException during
 713                // shutdown once the shutdown cancellation token is canceled.
 10714                throw;
 715            }
 0716            catch (ObjectDisposedException)
 0717            {
 718                // Do not log this exception. The AcceptAsync call can fail with ObjectDisposedException during
 719                // shutdown once the listener is disposed or if it is accepting a connection while the listener is
 720                // disposed.
 0721                throw;
 722            }
 0723            catch (Exception exception) when (IsRetryableAcceptException(exception))
 0724            {
 0725                _logger.LogConnectionAcceptFailedWithRetryableException(ServerAddress, exception);
 0726                throw;
 727            }
 0728            catch (Exception exception)
 0729            {
 0730                _logger.LogConnectionAcceptFailed(ServerAddress, exception);
 0731                throw;
 732            }
 8733        }
 734
 735        public ValueTask DisposeAsync()
 10736        {
 10737            _logger.LogStopAcceptingConnections(ServerAddress);
 10738            return _decoratee.DisposeAsync();
 10739        }
 740
 10741        internal LogConnectorListenerDecorator(IConnectorListener decoratee, ILogger logger)
 10742        {
 10743            _decoratee = decoratee;
 10744            _logger = logger;
 10745            _logger.LogStartAcceptingConnections(ServerAddress);
 10746        }
 747    }
 748
 749    private class LogConnectorDecorator : IConnector
 750    {
 751        private readonly IConnector _decoratee;
 752        private readonly ILogger _logger;
 753        private readonly EndPoint _remoteNetworkAddress;
 754        private readonly ServerAddress _serverAddress;
 755
 756        public async Task<TransportConnectionInformation> ConnectTransportConnectionAsync(
 757            CancellationToken cancellationToken)
 8758        {
 759            try
 8760            {
 8761                return await _decoratee.ConnectTransportConnectionAsync(cancellationToken).ConfigureAwait(false);
 762            }
 2763            catch (Exception exception)
 2764            {
 2765                _logger.LogConnectionConnectFailed(_serverAddress, _remoteNetworkAddress, exception);
 2766                throw;
 767            }
 6768        }
 769
 770        public IProtocolConnection CreateProtocolConnection(
 771            TransportConnectionInformation transportConnectionInformation) =>
 6772            new LogProtocolConnectionDecorator(
 6773                _decoratee.CreateProtocolConnection(transportConnectionInformation),
 6774                _serverAddress,
 6775                _remoteNetworkAddress,
 6776                _logger);
 777
 8778        public ValueTask DisposeAsync() => _decoratee.DisposeAsync();
 779
 780        public Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancel) =>
 0781            _decoratee.RefuseTransportConnectionAsync(serverBusy, cancel);
 782
 8783        internal LogConnectorDecorator(
 8784            IConnector decoratee,
 8785            ServerAddress serverAddress,
 8786            EndPoint remoteNetworkAddress,
 8787            ILogger logger)
 8788        {
 8789            _decoratee = decoratee;
 8790            _logger = logger;
 8791            _serverAddress = serverAddress;
 8792            _remoteNetworkAddress = remoteNetworkAddress;
 8793        }
 794    }
 795
 796    /// <summary>Provides a decorator that adds metrics to a <see cref="IConnectorListener" />.</summary>
 797    private class MetricsConnectorListenerDecorator : IConnectorListener
 798    {
 129799        public ServerAddress ServerAddress => _decoratee.ServerAddress;
 800
 801        private readonly IConnectorListener _decoratee;
 802
 803        public async Task<(IConnector, EndPoint)> AcceptAsync(
 804            CancellationToken cancellationToken)
 303805        {
 303806            (IConnector connector, EndPoint remoteNetworkAddress) =
 303807                await _decoratee.AcceptAsync(cancellationToken).ConfigureAwait(false);
 92808            return (new MetricsConnectorDecorator(connector), remoteNetworkAddress);
 92809        }
 810
 93811        public ValueTask DisposeAsync() => _decoratee.DisposeAsync();
 812
 93813        internal MetricsConnectorListenerDecorator(IConnectorListener decoratee) =>
 93814            _decoratee = decoratee;
 815    }
 816
 817    private class MetricsConnectorDecorator : IConnector
 818    {
 819        private readonly IConnector _decoratee;
 820
 821        public async Task<TransportConnectionInformation> ConnectTransportConnectionAsync(
 822            CancellationToken cancellationToken)
 92823        {
 92824            Metrics.ServerMetrics.ConnectStart();
 825            try
 92826            {
 92827                return await _decoratee.ConnectTransportConnectionAsync(cancellationToken).ConfigureAwait(false);
 828            }
 5829            catch
 5830            {
 5831                Metrics.ServerMetrics.ConnectStop();
 5832                Metrics.ServerMetrics.ConnectionFailure();
 5833                throw;
 834            }
 87835        }
 836
 837        public IProtocolConnection CreateProtocolConnection(
 838            TransportConnectionInformation transportConnectionInformation) =>
 80839                new MetricsProtocolConnectionDecorator(
 80840                    _decoratee.CreateProtocolConnection(transportConnectionInformation),
 80841                    Metrics.ServerMetrics,
 80842                    connectStarted: true);
 843
 92844        public ValueTask DisposeAsync() => _decoratee.DisposeAsync();
 845
 846        public async Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancel)
 7847        {
 848            try
 7849            {
 7850                await _decoratee.RefuseTransportConnectionAsync(serverBusy, cancel).ConfigureAwait(false);
 5851            }
 852            finally
 7853            {
 7854                Metrics.ServerMetrics.ConnectionFailure();
 7855                Metrics.ServerMetrics.ConnectStop();
 7856            }
 5857        }
 858
 184859        internal MetricsConnectorDecorator(IConnector decoratee) => _decoratee = decoratee;
 860    }
 861
 862    /// <summary>A connector listener accepts a transport connection and returns a <see cref="IConnector" />. The
 863    /// connector is used to refuse the transport connection or obtain a protocol connection once the transport
 864    /// connection is connected.</summary>
 865    private interface IConnectorListener : IAsyncDisposable
 866    {
 867        ServerAddress ServerAddress { get; }
 868
 869        Task<(IConnector, EndPoint)> AcceptAsync(CancellationToken cancel);
 870    }
 871
 872    /// <summary>A connector is returned by <see cref="IConnectorListener" />. The connector allows to connect the
 873    /// transport connection. If successful, the transport connection can either be refused or accepted by creating the
 874    /// protocol connection out of it.</summary>
 875    private interface IConnector : IAsyncDisposable
 876    {
 877        Task<TransportConnectionInformation> ConnectTransportConnectionAsync(CancellationToken cancellationToken);
 878
 879        IProtocolConnection CreateProtocolConnection(TransportConnectionInformation transportConnectionInformation);
 880
 881        Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancel);
 882    }
 883
 884    private class IceConnectorListener : IConnectorListener
 885    {
 43886        public ServerAddress ServerAddress { get; }
 887
 888        private readonly IListener<IDuplexConnection> _listener;
 889        private readonly ConnectionOptions _options;
 890
 25891        public ValueTask DisposeAsync() => _listener.DisposeAsync();
 892
 893        public async Task<(IConnector, EndPoint)> AcceptAsync(CancellationToken cancel)
 51894        {
 51895            (IDuplexConnection transportConnection, EndPoint remoteNetworkAddress) = await _listener.AcceptAsync(
 51896                cancel).ConfigureAwait(false);
 26897            return (new IceConnector(transportConnection, _options), remoteNetworkAddress);
 26898        }
 899
 25900        internal IceConnectorListener(
 25901            IListener<IDuplexConnection> listener,
 25902            ServerAddress serverAddress,
 25903            ConnectionOptions options)
 25904        {
 25905            _listener = listener;
 25906            ServerAddress = serverAddress with { Port = listener.TransportAddress.Port };
 25907            _options = options;
 25908        }
 909    }
 910
 911    private class IceConnector : IConnector
 912    {
 913        private readonly ConnectionOptions _options;
 914        private IDuplexConnection? _transportConnection;
 915
 916        public Task<TransportConnectionInformation> ConnectTransportConnectionAsync(
 917            CancellationToken cancellationToken) =>
 26918            _transportConnection!.ConnectAsync(cancellationToken);
 919
 920        public IProtocolConnection CreateProtocolConnection(
 921            TransportConnectionInformation transportConnectionInformation)
 23922        {
 923            // The protocol connection takes ownership of the transport connection.
 23924            var protocolConnection = new IceProtocolConnection(
 23925                _transportConnection!,
 23926                transportConnectionInformation,
 23927                _options);
 23928            _transportConnection = null;
 23929            return protocolConnection;
 23930        }
 931
 932        public ValueTask DisposeAsync()
 26933        {
 26934            _transportConnection?.Dispose();
 26935            return new();
 26936        }
 937
 938        public Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancellationToken)
 2939        {
 2940            _transportConnection!.Dispose();
 2941            return Task.CompletedTask;
 2942        }
 943
 26944        internal IceConnector(IDuplexConnection transportConnection, ConnectionOptions options)
 26945        {
 26946            _transportConnection = transportConnection;
 26947            _options = options;
 26948        }
 949    }
 950
 951    private class IceRpcConnectorListener : IConnectorListener
 952    {
 86953        public ServerAddress ServerAddress { get; }
 954
 955        private readonly IListener<IMultiplexedConnection> _listener;
 956        private readonly ConnectionOptions _options;
 957        private readonly ITaskExceptionObserver? _taskExceptionObserver;
 958
 959        public async Task<(IConnector, EndPoint)> AcceptAsync(CancellationToken cancel)
 252960        {
 252961            (IMultiplexedConnection transportConnection, EndPoint remoteNetworkAddress) = await _listener.AcceptAsync(
 252962                cancel).ConfigureAwait(false);
 66963            return (new IceRpcConnector(transportConnection, _options, _taskExceptionObserver), remoteNetworkAddress);
 66964        }
 965
 68966        public ValueTask DisposeAsync() => _listener.DisposeAsync();
 967
 68968        internal IceRpcConnectorListener(
 68969            IListener<IMultiplexedConnection> listener,
 68970            ServerAddress serverAddress,
 68971            ConnectionOptions options,
 68972            ITaskExceptionObserver? taskExceptionObserver)
 68973        {
 68974            _listener = listener;
 68975            ServerAddress = serverAddress with { Port = listener.TransportAddress.Port };
 68976            _options = options;
 68977            _taskExceptionObserver = taskExceptionObserver;
 68978        }
 979    }
 980
 981    private class IceRpcConnector : IConnector
 982    {
 983        private readonly ConnectionOptions _options;
 984        private readonly ITaskExceptionObserver? _taskExceptionObserver;
 985        private IMultiplexedConnection? _transportConnection;
 986
 987        public Task<TransportConnectionInformation> ConnectTransportConnectionAsync(
 988            CancellationToken cancellationToken) =>
 66989            _transportConnection!.ConnectAsync(cancellationToken);
 990
 991        public IProtocolConnection CreateProtocolConnection(
 992            TransportConnectionInformation transportConnectionInformation)
 57993        {
 994            // The protocol connection takes ownership of the transport connection.
 57995            var protocolConnection = new IceRpcProtocolConnection(
 57996                _transportConnection!,
 57997                transportConnectionInformation,
 57998                _options,
 57999                _taskExceptionObserver);
 571000            _transportConnection = null;
 571001            return protocolConnection;
 571002        }
 1003
 661004        public ValueTask DisposeAsync() => _transportConnection?.DisposeAsync() ?? new();
 1005
 1006        public Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancellationToken) =>
 51007            _transportConnection!.CloseAsync(
 51008                serverBusy ? MultiplexedConnectionCloseError.ServerBusy : MultiplexedConnectionCloseError.Refused,
 51009                cancellationToken);
 1010
 661011        internal IceRpcConnector(
 661012            IMultiplexedConnection transportConnection,
 661013            ConnectionOptions options,
 661014            ITaskExceptionObserver? taskExceptionObserver)
 661015        {
 661016            _transportConnection = transportConnection;
 661017            _options = options;
 661018            _taskExceptionObserver = taskExceptionObserver;
 661019        }
 1020    }
 1021}

Methods/Properties

.ctor(IceRpc.ServerOptions,IceRpc.Transports.IDuplexServerTransport,IceRpc.Transports.IMultiplexedServerTransport,Microsoft.Extensions.Logging.ILogger)
.ctor(IceRpc.IDispatcher,System.Net.Security.SslServerAuthenticationOptions,IceRpc.Transports.IDuplexServerTransport,IceRpc.Transports.IMultiplexedServerTransport,Microsoft.Extensions.Logging.ILogger)
.ctor(IceRpc.IDispatcher,IceRpc.ServerAddress,System.Net.Security.SslServerAuthenticationOptions,IceRpc.Transports.IDuplexServerTransport,IceRpc.Transports.IMultiplexedServerTransport,Microsoft.Extensions.Logging.ILogger)
.ctor(IceRpc.IDispatcher,System.Uri,System.Net.Security.SslServerAuthenticationOptions,IceRpc.Transports.IDuplexServerTransport,IceRpc.Transports.IMultiplexedServerTransport,Microsoft.Extensions.Logging.ILogger)
DisposeAsync()
PerformDisposeAsync()
Listen()
ListenAsync()
ConnectAsync()
DisposeDetachedConnectionAsync()
ShutdownWhenRequestedAsync()
ShutdownAsync(System.Threading.CancellationToken)
PerformShutdownAsync()
ToString()
IsRetryableAcceptException(System.Exception)
get_ServerAddress()
AcceptAsync()
DisposeAsync()
.ctor(IceRpc.Server/IConnectorListener,Microsoft.Extensions.Logging.ILogger)
ConnectTransportConnectionAsync()
CreateProtocolConnection(IceRpc.Transports.TransportConnectionInformation)
DisposeAsync()
RefuseTransportConnectionAsync(System.Boolean,System.Threading.CancellationToken)
.ctor(IceRpc.Server/IConnector,IceRpc.ServerAddress,System.Net.EndPoint,Microsoft.Extensions.Logging.ILogger)
get_ServerAddress()
AcceptAsync()
DisposeAsync()
.ctor(IceRpc.Server/IConnectorListener)
ConnectTransportConnectionAsync()
CreateProtocolConnection(IceRpc.Transports.TransportConnectionInformation)
DisposeAsync()
RefuseTransportConnectionAsync()
.ctor(IceRpc.Server/IConnector)
get_ServerAddress()
DisposeAsync()
AcceptAsync()
.ctor(IceRpc.Transports.IListener`1<IceRpc.Transports.IDuplexConnection>,IceRpc.ServerAddress,IceRpc.ConnectionOptions)
ConnectTransportConnectionAsync(System.Threading.CancellationToken)
CreateProtocolConnection(IceRpc.Transports.TransportConnectionInformation)
DisposeAsync()
RefuseTransportConnectionAsync(System.Boolean,System.Threading.CancellationToken)
.ctor(IceRpc.Transports.IDuplexConnection,IceRpc.ConnectionOptions)
get_ServerAddress()
AcceptAsync()
DisposeAsync()
.ctor(IceRpc.Transports.IListener`1<IceRpc.Transports.IMultiplexedConnection>,IceRpc.ServerAddress,IceRpc.ConnectionOptions,IceRpc.Internal.ITaskExceptionObserver)
ConnectTransportConnectionAsync(System.Threading.CancellationToken)
CreateProtocolConnection(IceRpc.Transports.TransportConnectionInformation)
DisposeAsync()
RefuseTransportConnectionAsync(System.Boolean,System.Threading.CancellationToken)
.ctor(IceRpc.Transports.IMultiplexedConnection,IceRpc.ConnectionOptions,IceRpc.Internal.ITaskExceptionObserver)