< Summary

Information
Class: IceRpc.Server
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Server.cs
Tag: 1986_28452893481
Line coverage
91%
Covered lines: 510
Uncovered lines: 48
Coverable lines: 558
Total lines: 1020
Line coverage: 91.3%
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.81%
.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 = options.ConnectionOptions.MaxIceRpcUnidirectionalStreams + 1,
 68146                        MinSegmentSize = options.ConnectionOptions.MinSegmentSize,
 68147                        Pool = options.ConnectionOptions.Pool
 68148                    },
 68149                    serverAuthenticationOptions);
 94150
 68151                listener = new IceRpcConnectorListener(
 68152                    transportListener,
 68153                    _serverAddress,
 68154                    options.ConnectionOptions,
 68155                    logger == NullLogger.Instance ? null : new LogTaskExceptionObserver(logger));
 68156            }
 94157
 93158            listener = new MetricsConnectorListenerDecorator(listener);
 93159            if (logger != NullLogger.Instance)
 10160            {
 10161                listener = new LogConnectorListenerDecorator(listener, logger);
 10162            }
 93163            return listener;
 187164        };
 94165    }
 166
 167    /// <summary>Constructs a server with the specified dispatcher and authentication options. All other properties
 168    /// use the <see cref="ServerOptions" /> defaults.</summary>
 169    /// <param name="dispatcher">The dispatcher of the server.</param>
 170    /// <param name="serverAuthenticationOptions">The SSL server authentication options. When not <see langword="null"
 171    /// />, the server will accept only secure connections.</param>
 172    /// <param name="duplexServerTransport">The transport used to create ice protocol connections. <see langword="null"
 173    /// /> is equivalent to <see cref="IDuplexServerTransport.Default" />.</param>
 174    /// <param name="multiplexedServerTransport">The transport used to create icerpc protocol connections. <see
 175    /// langword="null" /> is equivalent to <see cref="IMultiplexedServerTransport.Default" />.</param>
 176    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance"
 177    /// />.</param>
 178    public Server(
 179        IDispatcher dispatcher,
 180        SslServerAuthenticationOptions? serverAuthenticationOptions = null,
 181        IDuplexServerTransport? duplexServerTransport = null,
 182        IMultiplexedServerTransport? multiplexedServerTransport = null,
 183        ILogger? logger = null)
 1184        : this(
 1185            new ServerOptions
 1186            {
 1187                ServerAuthenticationOptions = serverAuthenticationOptions,
 1188                ConnectionOptions = new()
 1189                {
 1190                    Dispatcher = dispatcher,
 1191                }
 1192            },
 1193            duplexServerTransport,
 1194            multiplexedServerTransport,
 1195            logger)
 1196    {
 1197    }
 198
 199    /// <summary>Constructs a server with the specified dispatcher, server address and authentication options. All
 200    /// other properties use the <see cref="ServerOptions" /> defaults.</summary>
 201    /// <param name="dispatcher">The dispatcher of the server.</param>
 202    /// <param name="serverAddress">The server address of the server.</param>
 203    /// <param name="serverAuthenticationOptions">The SSL server authentication options. When not <see langword="null"
 204    /// />, the server will accept only secure connections.</param>
 205    /// <param name="duplexServerTransport">The transport used to create ice protocol connections. <see langword="null"
 206    /// /> is equivalent to <see cref="IDuplexServerTransport.Default" />.</param>
 207    /// <param name="multiplexedServerTransport">The transport used to create icerpc protocol connections. <see
 208    /// langword="null" /> is equivalent to <see cref="IMultiplexedServerTransport.Default" />.</param>
 209    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance"
 210    /// />.</param>
 211    public Server(
 212        IDispatcher dispatcher,
 213        ServerAddress serverAddress,
 214        SslServerAuthenticationOptions? serverAuthenticationOptions = null,
 215        IDuplexServerTransport? duplexServerTransport = null,
 216        IMultiplexedServerTransport? multiplexedServerTransport = null,
 217        ILogger? logger = null)
 34218        : this(
 34219            new ServerOptions
 34220            {
 34221                ServerAuthenticationOptions = serverAuthenticationOptions,
 34222                ConnectionOptions = new()
 34223                {
 34224                    Dispatcher = dispatcher,
 34225                },
 34226                ServerAddress = serverAddress
 34227            },
 34228            duplexServerTransport,
 34229            multiplexedServerTransport,
 34230            logger)
 34231    {
 34232    }
 233
 234    /// <summary>Constructs a server with the specified dispatcher, server address URI and authentication options. All
 235    /// other properties use the <see cref="ServerOptions" /> defaults.</summary>
 236    /// <param name="dispatcher">The dispatcher of the server.</param>
 237    /// <param name="serverAddressUri">A URI that represents the server address of the server.</param>
 238    /// <param name="serverAuthenticationOptions">The SSL server authentication options. When not <see langword="null"
 239    /// />, the server will accept only secure connections.</param>
 240    /// <param name="duplexServerTransport">The transport used to create ice protocol connections. <see langword="null"
 241    /// /> is equivalent to <see cref="IDuplexServerTransport.Default" />.</param>
 242    /// <param name="multiplexedServerTransport">The transport used to create icerpc protocol connections. <see
 243    /// langword="null" /> is equivalent to <see cref="IMultiplexedServerTransport.Default" />.</param>
 244    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance"
 245    /// />.</param>
 246    public Server(
 247        IDispatcher dispatcher,
 248        Uri serverAddressUri,
 249        SslServerAuthenticationOptions? serverAuthenticationOptions = null,
 250        IDuplexServerTransport? duplexServerTransport = null,
 251        IMultiplexedServerTransport? multiplexedServerTransport = null,
 252        ILogger? logger = null)
 14253        : this(
 14254            dispatcher,
 14255            new ServerAddress(serverAddressUri),
 14256            serverAuthenticationOptions,
 14257            duplexServerTransport,
 14258            multiplexedServerTransport,
 14259            logger)
 14260    {
 14261    }
 262
 263    /// <summary>Releases all resources allocated by this server. The server stops listening for new connections and
 264    /// disposes the connections it accepted from clients.</summary>
 265    /// <returns>A value task that completes when the disposal of all connections accepted by the server has completed.
 266    /// This includes connections that were active when this method is called and connections whose disposal was
 267    /// initiated prior to this call.</returns>
 268    /// <remarks>The disposal of an underlying connection of the server aborts invocations, cancels dispatches and
 269    /// disposes the underlying transport connection without waiting for the peer. To wait for invocations and
 270    /// dispatches to complete, call <see cref="ShutdownAsync" /> first. If the configured dispatcher does not complete
 271    /// promptly when its cancellation token is canceled, the disposal can hang.</remarks>
 272    public ValueTask DisposeAsync()
 95273    {
 274        lock (_mutex)
 95275        {
 95276            if (_disposeTask is null)
 94277            {
 94278                _shutdownTask ??= Task.CompletedTask;
 94279                if (_detachedConnectionCount == 0)
 85280                {
 85281                    _ = _detachedConnectionsTcs.TrySetResult();
 85282                }
 283
 94284                _disposeTask = PerformDisposeAsync();
 94285            }
 95286            return new(_disposeTask);
 287        }
 288
 289        async Task PerformDisposeAsync()
 94290        {
 94291            await Task.Yield(); // exit mutex lock
 292
 94293            _disposedCts.Cancel();
 294
 295            // _listenTask etc are immutable when _disposeTask is not null.
 296
 297            // Wait for shutdown before disposing connections.
 298            try
 94299            {
 94300                await _shutdownTask.ConfigureAwait(false);
 94301            }
 0302            catch
 0303            {
 304                // Ignore exceptions.
 0305            }
 306
 94307            if (_listenTask is not null)
 93308            {
 309                try
 93310                {
 93311                    await _listenTask.ConfigureAwait(false);
 93312                }
 0313                catch
 0314                {
 315                    // Ignore exceptions.
 0316                }
 317
 93318                await Task.WhenAll(
 93319                    _connections
 49320                        .Select(connection => connection.DisposeAsync().AsTask())
 93321                        .Append(_detachedConnectionsTcs.Task)).ConfigureAwait(false);
 93322            }
 323
 94324            _disposedCts.Dispose();
 94325            _shutdownCts.Dispose();
 94326        }
 95327    }
 328
 329    /// <summary>Starts accepting connections on the configured server address. Requests received over these connections
 330    /// are then dispatched by the configured dispatcher.</summary>
 331    /// <returns>The server address this server is listening on and that a client would connect to. This address is the
 332    /// same as the <see cref="ServerOptions.ServerAddress" /> of <see cref="ServerOptions" /> except its
 333    /// <see cref="ServerAddress.Transport" /> property is always non-null and its port number is never 0 when the host
 334    /// is an IP address.</returns>
 335    /// <exception cref="IceRpcException">Thrown when the server transport fails to listen on the configured <see
 336    /// cref="ServerOptions.ServerAddress" />.</exception>
 337    /// <exception cref="InvalidOperationException">Thrown when the server is already listening, shut down or shutting
 338    /// down.</exception>
 339    /// <exception cref="ObjectDisposedException">Throw when the server is disposed.</exception>
 340    /// <remarks><see cref="Listen" /> can also throw exceptions from the transport; for example, the transport can
 341    /// reject the server address.</remarks>
 342    public ServerAddress Listen()
 95343    {
 344        lock (_mutex)
 95345        {
 95346            ObjectDisposedException.ThrowIf(_disposeTask is not null, this);
 347
 94348            if (_shutdownTask is not null)
 0349            {
 0350                throw new InvalidOperationException($"Server '{this}' is shut down or shutting down.");
 351            }
 94352            if (_listenTask is not null)
 1353            {
 1354                throw new InvalidOperationException($"Server '{this}' is already listening.");
 355            }
 356
 93357            IConnectorListener listener = _listenerFactory();
 93358            _listenTask = ListenAsync(listener); // _listenTask owns listener and must dispose it
 93359            return listener.ServerAddress;
 360        }
 361
 362        async Task ListenAsync(IConnectorListener listener)
 93363        {
 93364            await Task.Yield(); // exit mutex lock
 365
 366            try
 93367            {
 93368                using var pendingConnectionSemaphore = new SemaphoreSlim(
 93369                    _maxPendingConnections,
 93370                    _maxPendingConnections);
 371
 185372                while (!_shutdownCts.IsCancellationRequested)
 185373                {
 185374                    await pendingConnectionSemaphore.WaitAsync(_shutdownCts.Token).ConfigureAwait(false);
 375
 184376                    IConnector? connector = null;
 377                    do
 199378                    {
 379                        try
 199380                        {
 199381                            (connector, _) = await listener.AcceptAsync(_shutdownCts.Token).ConfigureAwait(false);
 92382                        }
 107383                        catch (Exception exception) when (IsRetryableAcceptException(exception))
 15384                        {
 385                            // continue
 15386                        }
 107387                    }
 107388                    while (connector is null);
 389
 390                    // We don't wait for the connection to be activated or shutdown. This could take a while for some
 391                    // transports such as TLS based transports where the handshake requires few round trips between the
 392                    // client and server. Waiting could also cause a security issue if the client doesn't respond to the
 393                    // connection initialization as we wouldn't be able to accept new connections in the meantime. The
 394                    // call will eventually timeout if the ConnectTimeout expires.
 92395                    CancellationToken cancellationToken = _disposedCts.Token;
 92396                    _ = Task.Run(
 92397                        async () =>
 92398                        {
 92399                            try
 92400                            {
 92401                                await ConnectAsync(connector, cancellationToken).ConfigureAwait(false);
 87402                            }
 5403                            catch
 5404                            {
 92405                                // Ignore connection establishment failure. This failures are logged by the
 92406                                // LogConnectorDecorator
 5407                            }
 92408                            finally
 92409                            {
 92410                                // The connection dispose will dispose the transport connection if it has not been
 92411                                // adopted by the protocol connection.
 92412                                await connector.DisposeAsync().ConfigureAwait(false);
 92413
 92414                                // The pending connection semaphore is disposed by the listen task completion once
 92415                                // shutdown / dispose is initiated.
 92416                                lock (_mutex)
 92417                                {
 92418                                    if (_shutdownTask is null)
 88419                                    {
 88420                                        pendingConnectionSemaphore.Release();
 88421                                    }
 92422                                }
 92423                            }
 92424                        },
 92425                        CancellationToken.None); // the task must run to dispose the connector.
 92426                }
 0427            }
 93428            catch
 93429            {
 430                // Ignore. Exceptions thrown by listener.AcceptAsync are logged by the log decorator when appropriate.
 93431            }
 432            finally
 93433            {
 93434                await listener.DisposeAsync().ConfigureAwait(false);
 93435            }
 436
 437            async Task ConnectAsync(IConnector connector, CancellationToken cancellationToken)
 92438            {
 92439                using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 92440                connectCts.CancelAfter(_connectTimeout);
 441
 442                // Connect the transport connection first. This connection establishment can be interrupted by the
 443                // connect timeout or the server ShutdownAsync/DisposeAsync.
 92444                TransportConnectionInformation transportConnectionInformation =
 92445                    await connector.ConnectTransportConnectionAsync(connectCts.Token).ConfigureAwait(false);
 446
 87447                IProtocolConnection? protocolConnection = null;
 87448                bool serverBusy = false;
 449
 450                lock (_mutex)
 87451                {
 87452                    Debug.Assert(
 87453                        _maxConnections == 0 || _connections.Count + _detachedConnectionCount <= _maxConnections);
 454
 87455                    if (_shutdownTask is null)
 87456                    {
 87457                        if (_maxConnections > 0 && (_connections.Count + _detachedConnectionCount) == _maxConnections)
 7458                        {
 7459                            serverBusy = true;
 7460                        }
 461                        else
 80462                        {
 463                            // The protocol connection adopts the transport connection from the connector and it's
 464                            // now responsible for disposing of it.
 80465                            protocolConnection = connector.CreateProtocolConnection(transportConnectionInformation);
 80466                            _detachedConnectionCount++;
 80467                        }
 87468                    }
 87469                }
 470
 87471                if (protocolConnection is null)
 7472                {
 473                    try
 7474                    {
 7475                        await connector.RefuseTransportConnectionAsync(serverBusy, connectCts.Token)
 7476                            .ConfigureAwait(false);
 5477                    }
 2478                    catch
 2479                    {
 480                        // ignore and continue
 2481                    }
 482                    // The transport connection is disposed by the disposal of the connector.
 7483                }
 484                else
 80485                {
 486                    Task shutdownRequested;
 487                    try
 80488                    {
 80489                        (_, shutdownRequested) = await protocolConnection.ConnectAsync(connectCts.Token)
 80490                            .ConfigureAwait(false);
 80491                    }
 0492                    catch
 0493                    {
 0494                        await DisposeDetachedConnectionAsync(protocolConnection, withShutdown: false)
 0495                            .ConfigureAwait(false);
 0496                        throw;
 497                    }
 498
 80499                    LinkedListNode<IProtocolConnection>? listNode = null;
 500
 501                    lock (_mutex)
 502                    {
 80503                        if (_shutdownTask is null)
 504                        {
 79505                            listNode = _connections.AddLast(protocolConnection);
 506
 507                            // protocolConnection is no longer a detached connection since it's now "attached" in
 508                            // _connections.
 79509                            _detachedConnectionCount--;
 510                        }
 80511                    }
 512
 80513                    if (listNode is null)
 514                    {
 1515                        await DisposeDetachedConnectionAsync(protocolConnection, withShutdown: true)
 1516                            .ConfigureAwait(false);
 517                    }
 518                    else
 79519                    {
 520                        // Schedule removal after successful ConnectAsync.
 79521                        _ = ShutdownWhenRequestedAsync(protocolConnection, shutdownRequested, listNode);
 522                    }
 80523                }
 524            }
 525        }
 526
 527        async Task DisposeDetachedConnectionAsync(IProtocolConnection connection, bool withShutdown)
 31528        {
 31529            if (withShutdown)
 31530            {
 531                // _disposedCts is not disposed since we own a _backgroundConnectionDisposeCount.
 31532                using var cts = CancellationTokenSource.CreateLinkedTokenSource(_disposedCts.Token);
 31533                cts.CancelAfter(_shutdownTimeout);
 534
 535                try
 31536                {
 537                    // Can be canceled by DisposeAsync or the shutdown timeout.
 31538                    await connection.ShutdownAsync(cts.Token).ConfigureAwait(false);
 21539                }
 10540                catch
 10541                {
 542                    // Ignore connection shutdown failures. connection.ShutdownAsync makes sure it's an "expected"
 543                    // exception.
 10544                }
 31545            }
 546
 31547            await connection.DisposeAsync().ConfigureAwait(false);
 548            lock (_mutex)
 31549            {
 31550                if (--_detachedConnectionCount == 0 && _shutdownTask is not null)
 15551                {
 15552                    _detachedConnectionsTcs.SetResult();
 15553                }
 31554            }
 31555        }
 556
 557        // Remove the connection from _connections after a successful ConnectAsync.
 558        async Task ShutdownWhenRequestedAsync(
 559            IProtocolConnection connection,
 560            Task shutdownRequested,
 561            LinkedListNode<IProtocolConnection> listNode)
 79562        {
 79563            await shutdownRequested.ConfigureAwait(false);
 564
 565            lock (_mutex)
 69566            {
 69567                if (_shutdownTask is null)
 30568                {
 30569                    _connections.Remove(listNode);
 30570                    _detachedConnectionCount++;
 30571                }
 572                else
 39573                {
 574                    // _connections is immutable and ShutdownAsync/DisposeAsync is responsible to shutdown/dispose
 575                    // this connection.
 39576                    return;
 577                }
 30578            }
 579
 30580            await DisposeDetachedConnectionAsync(connection, withShutdown: true).ConfigureAwait(false);
 69581        }
 273582    }
 583
 584    /// <summary>Gracefully shuts down this server: the server stops accepting new connections and shuts down gracefully
 585    /// all its connections.</summary>
 586    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 587    /// <returns>A task that completes successfully once the shutdown of all connections accepted by the server has
 588    /// completed. This includes connections that were active when this method is called and connections whose shutdown
 589    /// was initiated prior to this call.</returns>
 590    /// <exception cref="InvalidOperationException">Thrown if this method is called more than once.</exception>
 591    /// <exception cref="ObjectDisposedException">Thrown if the server is disposed.</exception>
 592    /// <remarks><para>The returned task can also complete with one of the following exceptions:</para>
 593    /// <list type="bullet">
 594    /// <item><description><see cref="IceRpcException" /> with error <see cref="IceRpcError.OperationAborted" /> if the
 595    /// server is disposed while being shut down.</description></item>
 596    /// <item><description><see cref="OperationCanceledException" /> if cancellation was requested through the
 597    /// cancellation token.</description></item>
 598    /// <item><description><see cref="TimeoutException" /> if the shutdown timed out.</description></item>
 599    /// </list>
 600    /// </remarks>
 601    public Task ShutdownAsync(CancellationToken cancellationToken = default)
 34602    {
 603        lock (_mutex)
 34604        {
 34605            ObjectDisposedException.ThrowIf(_disposeTask is not null, this);
 606
 34607            if (_shutdownTask is not null)
 0608            {
 0609                throw new InvalidOperationException($"Server '{this}' is shut down or shutting down.");
 610            }
 611
 34612            if (_detachedConnectionCount == 0)
 28613            {
 28614                _detachedConnectionsTcs.SetResult();
 28615            }
 616
 34617            _shutdownTask = PerformShutdownAsync();
 34618        }
 34619        return _shutdownTask;
 620
 621        async Task PerformShutdownAsync()
 34622        {
 34623            await Task.Yield(); // exit mutex lock
 624
 34625            _shutdownCts.Cancel();
 626
 627            // _listenTask is immutable once _shutdownTask is not null.
 34628            if (_listenTask is not null)
 34629            {
 630                try
 34631                {
 34632                    using var cts = CancellationTokenSource.CreateLinkedTokenSource(
 34633                        cancellationToken,
 34634                        _disposedCts.Token);
 635
 34636                    cts.CancelAfter(_shutdownTimeout);
 637
 638                    try
 34639                    {
 34640                        await Task.WhenAll(
 34641                            _connections
 14642                                .Select(connection => connection.ShutdownAsync(cts.Token))
 34643                                .Append(_listenTask.WaitAsync(cts.Token))
 34644                                .Append(_detachedConnectionsTcs.Task.WaitAsync(cts.Token)))
 34645                            .ConfigureAwait(false);
 33646                    }
 0647                    catch (OperationCanceledException)
 0648                    {
 0649                        throw;
 650                    }
 1651                    catch
 1652                    {
 653                        // Ignore _listenTask and connection shutdown exceptions
 654
 655                        // Throw OperationCanceledException if this WhenAll exception is hiding an OCE.
 1656                        cts.Token.ThrowIfCancellationRequested();
 1657                    }
 34658                }
 0659                catch (OperationCanceledException)
 0660                {
 0661                    cancellationToken.ThrowIfCancellationRequested();
 662
 0663                    if (_disposedCts.IsCancellationRequested)
 0664                    {
 0665                        throw new IceRpcException(
 0666                            IceRpcError.OperationAborted,
 0667                            "The shutdown was aborted because the server was disposed.");
 668                    }
 669                    else
 0670                    {
 0671                        throw new TimeoutException(
 0672                            $"The server shut down timed out after {_shutdownTimeout.TotalSeconds} s.");
 673                    }
 674                }
 34675            }
 34676        }
 34677    }
 678
 679    /// <summary>Returns a string that represents this server.</summary>
 680    /// <returns>A string that represents this server.</returns>
 1681    public override string ToString() => _serverAddress.ToString();
 682
 683    /// <summary>Returns true if the <see cref="IConnectorListener.AcceptAsync" /> failure can be retried.</summary>
 684    private static bool IsRetryableAcceptException(Exception exception) =>
 685        // Transports such as QUIC do the SSL handshake when the connection is accepted, this can throw
 686        // AuthenticationException if it fails.
 107687        exception is IceRpcException or AuthenticationException;
 688
 689    /// <summary>Provides a decorator that adds logging to a <see cref="IConnectorListener" />.</summary>
 690    private class LogConnectorListenerDecorator : IConnectorListener
 691    {
 46692        public ServerAddress ServerAddress => _decoratee.ServerAddress;
 693
 694        private readonly IConnectorListener _decoratee;
 695        private readonly ILogger _logger;
 696
 697        public async Task<(IConnector, EndPoint)> AcceptAsync(CancellationToken cancellationToken)
 18698        {
 699            try
 18700            {
 18701                (IConnector connector, EndPoint remoteNetworkAddress) =
 18702                    await _decoratee.AcceptAsync(cancellationToken).ConfigureAwait(false);
 703
 8704                _logger.LogConnectionAccepted(ServerAddress, remoteNetworkAddress);
 8705                return (
 8706                    new LogConnectorDecorator(connector, ServerAddress, remoteNetworkAddress, _logger),
 8707                    remoteNetworkAddress);
 708            }
 10709            catch (OperationCanceledException exception) when (exception.CancellationToken == cancellationToken)
 10710            {
 711                // Do not log this exception. The AcceptAsync call can fail with OperationCanceledException during
 712                // shutdown once the shutdown cancellation token is canceled.
 10713                throw;
 714            }
 0715            catch (ObjectDisposedException)
 0716            {
 717                // Do not log this exception. The AcceptAsync call can fail with ObjectDisposedException during
 718                // shutdown once the listener is disposed or if it is accepting a connection while the listener is
 719                // disposed.
 0720                throw;
 721            }
 0722            catch (Exception exception) when (IsRetryableAcceptException(exception))
 0723            {
 0724                _logger.LogConnectionAcceptFailedWithRetryableException(ServerAddress, exception);
 0725                throw;
 726            }
 0727            catch (Exception exception)
 0728            {
 0729                _logger.LogConnectionAcceptFailed(ServerAddress, exception);
 0730                throw;
 731            }
 8732        }
 733
 734        public ValueTask DisposeAsync()
 10735        {
 10736            _logger.LogStopAcceptingConnections(ServerAddress);
 10737            return _decoratee.DisposeAsync();
 10738        }
 739
 10740        internal LogConnectorListenerDecorator(IConnectorListener decoratee, ILogger logger)
 10741        {
 10742            _decoratee = decoratee;
 10743            _logger = logger;
 10744            _logger.LogStartAcceptingConnections(ServerAddress);
 10745        }
 746    }
 747
 748    private class LogConnectorDecorator : IConnector
 749    {
 750        private readonly IConnector _decoratee;
 751        private readonly ILogger _logger;
 752        private readonly EndPoint _remoteNetworkAddress;
 753        private readonly ServerAddress _serverAddress;
 754
 755        public async Task<TransportConnectionInformation> ConnectTransportConnectionAsync(
 756            CancellationToken cancellationToken)
 8757        {
 758            try
 8759            {
 8760                return await _decoratee.ConnectTransportConnectionAsync(cancellationToken).ConfigureAwait(false);
 761            }
 2762            catch (Exception exception)
 2763            {
 2764                _logger.LogConnectionConnectFailed(_serverAddress, _remoteNetworkAddress, exception);
 2765                throw;
 766            }
 6767        }
 768
 769        public IProtocolConnection CreateProtocolConnection(
 770            TransportConnectionInformation transportConnectionInformation) =>
 6771            new LogProtocolConnectionDecorator(
 6772                _decoratee.CreateProtocolConnection(transportConnectionInformation),
 6773                _serverAddress,
 6774                _remoteNetworkAddress,
 6775                _logger);
 776
 8777        public ValueTask DisposeAsync() => _decoratee.DisposeAsync();
 778
 779        public Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancel) =>
 0780            _decoratee.RefuseTransportConnectionAsync(serverBusy, cancel);
 781
 8782        internal LogConnectorDecorator(
 8783            IConnector decoratee,
 8784            ServerAddress serverAddress,
 8785            EndPoint remoteNetworkAddress,
 8786            ILogger logger)
 8787        {
 8788            _decoratee = decoratee;
 8789            _logger = logger;
 8790            _serverAddress = serverAddress;
 8791            _remoteNetworkAddress = remoteNetworkAddress;
 8792        }
 793    }
 794
 795    /// <summary>Provides a decorator that adds metrics to a <see cref="IConnectorListener" />.</summary>
 796    private class MetricsConnectorListenerDecorator : IConnectorListener
 797    {
 129798        public ServerAddress ServerAddress => _decoratee.ServerAddress;
 799
 800        private readonly IConnectorListener _decoratee;
 801
 802        public async Task<(IConnector, EndPoint)> AcceptAsync(
 803            CancellationToken cancellationToken)
 199804        {
 199805            (IConnector connector, EndPoint remoteNetworkAddress) =
 199806                await _decoratee.AcceptAsync(cancellationToken).ConfigureAwait(false);
 92807            return (new MetricsConnectorDecorator(connector), remoteNetworkAddress);
 92808        }
 809
 93810        public ValueTask DisposeAsync() => _decoratee.DisposeAsync();
 811
 93812        internal MetricsConnectorListenerDecorator(IConnectorListener decoratee) =>
 93813            _decoratee = decoratee;
 814    }
 815
 816    private class MetricsConnectorDecorator : IConnector
 817    {
 818        private readonly IConnector _decoratee;
 819
 820        public async Task<TransportConnectionInformation> ConnectTransportConnectionAsync(
 821            CancellationToken cancellationToken)
 92822        {
 92823            Metrics.ServerMetrics.ConnectStart();
 824            try
 92825            {
 92826                return await _decoratee.ConnectTransportConnectionAsync(cancellationToken).ConfigureAwait(false);
 827            }
 5828            catch
 5829            {
 5830                Metrics.ServerMetrics.ConnectStop();
 5831                Metrics.ServerMetrics.ConnectionFailure();
 5832                throw;
 833            }
 87834        }
 835
 836        public IProtocolConnection CreateProtocolConnection(
 837            TransportConnectionInformation transportConnectionInformation) =>
 80838                new MetricsProtocolConnectionDecorator(
 80839                    _decoratee.CreateProtocolConnection(transportConnectionInformation),
 80840                    Metrics.ServerMetrics,
 80841                    connectStarted: true);
 842
 92843        public ValueTask DisposeAsync() => _decoratee.DisposeAsync();
 844
 845        public async Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancel)
 7846        {
 847            try
 7848            {
 7849                await _decoratee.RefuseTransportConnectionAsync(serverBusy, cancel).ConfigureAwait(false);
 5850            }
 851            finally
 7852            {
 7853                Metrics.ServerMetrics.ConnectionFailure();
 7854                Metrics.ServerMetrics.ConnectStop();
 7855            }
 5856        }
 857
 184858        internal MetricsConnectorDecorator(IConnector decoratee) => _decoratee = decoratee;
 859    }
 860
 861    /// <summary>A connector listener accepts a transport connection and returns a <see cref="IConnector" />. The
 862    /// connector is used to refuse the transport connection or obtain a protocol connection once the transport
 863    /// connection is connected.</summary>
 864    private interface IConnectorListener : IAsyncDisposable
 865    {
 866        ServerAddress ServerAddress { get; }
 867
 868        Task<(IConnector, EndPoint)> AcceptAsync(CancellationToken cancel);
 869    }
 870
 871    /// <summary>A connector is returned by <see cref="IConnectorListener" />. The connector allows to connect the
 872    /// transport connection. If successful, the transport connection can either be refused or accepted by creating the
 873    /// protocol connection out of it.</summary>
 874    private interface IConnector : IAsyncDisposable
 875    {
 876        Task<TransportConnectionInformation> ConnectTransportConnectionAsync(CancellationToken cancellationToken);
 877
 878        IProtocolConnection CreateProtocolConnection(TransportConnectionInformation transportConnectionInformation);
 879
 880        Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancel);
 881    }
 882
 883    private class IceConnectorListener : IConnectorListener
 884    {
 43885        public ServerAddress ServerAddress { get; }
 886
 887        private readonly IListener<IDuplexConnection> _listener;
 888        private readonly ConnectionOptions _options;
 889
 25890        public ValueTask DisposeAsync() => _listener.DisposeAsync();
 891
 892        public async Task<(IConnector, EndPoint)> AcceptAsync(CancellationToken cancel)
 51893        {
 51894            (IDuplexConnection transportConnection, EndPoint remoteNetworkAddress) = await _listener.AcceptAsync(
 51895                cancel).ConfigureAwait(false);
 26896            return (new IceConnector(transportConnection, _options), remoteNetworkAddress);
 26897        }
 898
 25899        internal IceConnectorListener(
 25900            IListener<IDuplexConnection> listener,
 25901            ServerAddress serverAddress,
 25902            ConnectionOptions options)
 25903        {
 25904            _listener = listener;
 25905            ServerAddress = serverAddress with { Port = listener.TransportAddress.Port };
 25906            _options = options;
 25907        }
 908    }
 909
 910    private class IceConnector : IConnector
 911    {
 912        private readonly ConnectionOptions _options;
 913        private IDuplexConnection? _transportConnection;
 914
 915        public Task<TransportConnectionInformation> ConnectTransportConnectionAsync(
 916            CancellationToken cancellationToken) =>
 26917            _transportConnection!.ConnectAsync(cancellationToken);
 918
 919        public IProtocolConnection CreateProtocolConnection(
 920            TransportConnectionInformation transportConnectionInformation)
 23921        {
 922            // The protocol connection takes ownership of the transport connection.
 23923            var protocolConnection = new IceProtocolConnection(
 23924                _transportConnection!,
 23925                transportConnectionInformation,
 23926                _options);
 23927            _transportConnection = null;
 23928            return protocolConnection;
 23929        }
 930
 931        public ValueTask DisposeAsync()
 26932        {
 26933            _transportConnection?.Dispose();
 26934            return new();
 26935        }
 936
 937        public Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancellationToken)
 2938        {
 2939            _transportConnection!.Dispose();
 2940            return Task.CompletedTask;
 2941        }
 942
 26943        internal IceConnector(IDuplexConnection transportConnection, ConnectionOptions options)
 26944        {
 26945            _transportConnection = transportConnection;
 26946            _options = options;
 26947        }
 948    }
 949
 950    private class IceRpcConnectorListener : IConnectorListener
 951    {
 86952        public ServerAddress ServerAddress { get; }
 953
 954        private readonly IListener<IMultiplexedConnection> _listener;
 955        private readonly ConnectionOptions _options;
 956        private readonly ITaskExceptionObserver? _taskExceptionObserver;
 957
 958        public async Task<(IConnector, EndPoint)> AcceptAsync(CancellationToken cancel)
 148959        {
 148960            (IMultiplexedConnection transportConnection, EndPoint remoteNetworkAddress) = await _listener.AcceptAsync(
 148961                cancel).ConfigureAwait(false);
 66962            return (new IceRpcConnector(transportConnection, _options, _taskExceptionObserver), remoteNetworkAddress);
 66963        }
 964
 68965        public ValueTask DisposeAsync() => _listener.DisposeAsync();
 966
 68967        internal IceRpcConnectorListener(
 68968            IListener<IMultiplexedConnection> listener,
 68969            ServerAddress serverAddress,
 68970            ConnectionOptions options,
 68971            ITaskExceptionObserver? taskExceptionObserver)
 68972        {
 68973            _listener = listener;
 68974            ServerAddress = serverAddress with { Port = listener.TransportAddress.Port };
 68975            _options = options;
 68976            _taskExceptionObserver = taskExceptionObserver;
 68977        }
 978    }
 979
 980    private class IceRpcConnector : IConnector
 981    {
 982        private readonly ConnectionOptions _options;
 983        private readonly ITaskExceptionObserver? _taskExceptionObserver;
 984        private IMultiplexedConnection? _transportConnection;
 985
 986        public Task<TransportConnectionInformation> ConnectTransportConnectionAsync(
 987            CancellationToken cancellationToken) =>
 66988            _transportConnection!.ConnectAsync(cancellationToken);
 989
 990        public IProtocolConnection CreateProtocolConnection(
 991            TransportConnectionInformation transportConnectionInformation)
 57992        {
 993            // The protocol connection takes ownership of the transport connection.
 57994            var protocolConnection = new IceRpcProtocolConnection(
 57995                _transportConnection!,
 57996                transportConnectionInformation,
 57997                _options,
 57998                _taskExceptionObserver);
 57999            _transportConnection = null;
 571000            return protocolConnection;
 571001        }
 1002
 661003        public ValueTask DisposeAsync() => _transportConnection?.DisposeAsync() ?? new();
 1004
 1005        public Task RefuseTransportConnectionAsync(bool serverBusy, CancellationToken cancellationToken) =>
 51006            _transportConnection!.CloseAsync(
 51007                serverBusy ? MultiplexedConnectionCloseError.ServerBusy : MultiplexedConnectionCloseError.Refused,
 51008                cancellationToken);
 1009
 661010        internal IceRpcConnector(
 661011            IMultiplexedConnection transportConnection,
 661012            ConnectionOptions options,
 661013            ITaskExceptionObserver? taskExceptionObserver)
 661014        {
 661015            _transportConnection = transportConnection;
 661016            _options = options;
 661017            _taskExceptionObserver = taskExceptionObserver;
 661018        }
 1019    }
 1020}

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)