< Summary

Information
Class: IceRpc.ClientConnection
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/ClientConnection.cs
Tag: 1986_28452893481
Line coverage
82%
Covered lines: 292
Uncovered lines: 63
Coverable lines: 355
Total lines: 685
Line coverage: 82.2%
Branch coverage
76%
Covered branches: 75
Total branches: 98
Branch coverage: 76.5%
Method coverage
100%
Covered methods: 19
Fully covered methods: 6
Total methods: 19
Method coverage: 100%
Full method coverage: 31.5%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)90%1010100%
.ctor(...)100%11100%
.ctor(...)100%11100%
ConnectAsync(...)66.66%6677.27%
PerformConnectAsync()100%1170%
DisposeAsync()100%66100%
PerformDisposeAsync()100%44100%
InvokeAsync(...)57.14%231463.63%
CheckRequestServerAddresses()83.33%6692.3%
PerformInvokeAsync()75%6453.84%
ShutdownAsync(...)75%4484.61%
PerformShutdownAsync()90%121073.07%
CreateConnectTask()66.66%121288.88%
DisposePendingConnectionAsync()100%4477.77%
ShutdownWhenRequestedAsync()100%11100%
RemoveFromActiveAsync(...)83.33%66100%
ShutdownAndDisposeConnectionAsync()50%4482.35%
GetActiveConnectionAsync(...)62.5%9873.91%
PerformGetActiveConnectionAsync()100%1150%

File(s)

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

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Features;
 4using IceRpc.Transports;
 5using Microsoft.Extensions.Logging;
 6using Microsoft.Extensions.Logging.Abstractions;
 7using System.Collections.Immutable;
 8using System.Diagnostics;
 9using System.Net.Security;
 10using System.Security.Authentication;
 11
 12namespace IceRpc;
 13
 14/// <summary>Represents a client connection used to send requests to a server and receive the corresponding responses.
 15/// </summary>
 16/// <remarks>This client connection can also dispatch requests ("callbacks") received from the server. The client
 17/// connection's underlying connection is recreated and reconnected automatically when it's closed by any event other
 18/// than a call to <see cref="ShutdownAsync" /> or <see cref="DisposeAsync" />.</remarks>
 19public sealed class ClientConnection : IInvoker, IAsyncDisposable
 20{
 21    // The underlying protocol connection once successfully established.
 22    private (IProtocolConnection Connection, TransportConnectionInformation ConnectionInformation)? _activeConnection;
 23
 24    private readonly IClientProtocolConnectionFactory _clientProtocolConnectionFactory;
 25
 26    private readonly TimeSpan _connectTimeout;
 27
 28    // A detached connection is a protocol connection that is connecting, shutting down or being disposed. Both
 29    // ShutdownAsync and DisposeAsync wait for detached connections to reach 0 using _detachedConnectionsTcs. Such a
 30    // connection is "detached" because it's not in _activeConnection.
 31    private int _detachedConnectionCount;
 32
 9133    private readonly TaskCompletionSource _detachedConnectionsTcs =
 9134        new(TaskCreationOptions.RunContinuationsAsynchronously);
 35
 36    // A cancellation token source that is canceled when DisposeAsync is called.
 9137    private readonly CancellationTokenSource _disposedCts = new();
 38    private Task? _disposeTask;
 39
 9140    private readonly Lock _mutex = new();
 41
 42    // A connection being established and its associated connect task. When non-null, _activeConnection is null.
 43    private (IProtocolConnection Connection, Task<TransportConnectionInformation> ConnectTask)? _pendingConnection;
 44
 45    private Task? _shutdownTask;
 46
 47    private readonly TimeSpan _shutdownTimeout;
 48
 49    private readonly ServerAddress _serverAddress;
 50
 51    /// <summary>Constructs a client connection.</summary>
 52    /// <param name="options">The client connection options.</param>
 53    /// <param name="duplexClientTransport">The duplex client transport. <see langword="null" /> is equivalent to <see
 54    /// cref="IDuplexClientTransport.Default" />.</param>
 55    /// <param name="multiplexedClientTransport">The multiplexed client transport. <see langword="null" /> is equivalent
 56    /// to <see cref="IMultiplexedClientTransport.Default" />.</param>
 57    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance" />.
 58    /// </param>
 9159    public ClientConnection(
 9160        ClientConnectionOptions options,
 9161        IDuplexClientTransport? duplexClientTransport = null,
 9162        IMultiplexedClientTransport? multiplexedClientTransport = null,
 9163        ILogger? logger = null)
 9164    {
 9165        _connectTimeout = options.ConnectTimeout;
 9166        _shutdownTimeout = options.ShutdownTimeout;
 67
 9168        duplexClientTransport ??= IDuplexClientTransport.Default;
 9169        multiplexedClientTransport ??= IMultiplexedClientTransport.Default;
 70
 9171        _serverAddress = options.ServerAddress ??
 9172            throw new ArgumentException(
 9173                $"{nameof(ClientConnectionOptions.ServerAddress)} is not set",
 9174                nameof(options));
 75
 9176        if (_serverAddress.Transport is null)
 5077        {
 5078            _serverAddress = _serverAddress with
 5079            {
 5080                Transport = _serverAddress.Protocol == Protocol.Ice ?
 5081                    duplexClientTransport.DefaultName : multiplexedClientTransport.DefaultName
 5082            };
 5083        }
 84
 9185        _clientProtocolConnectionFactory = new ClientProtocolConnectionFactory(
 9186            options,
 9187            options.ConnectTimeout,
 9188            options.ClientAuthenticationOptions,
 9189            duplexClientTransport,
 9190            multiplexedClientTransport,
 9191            logger);
 9192    }
 93
 94    /// <summary>Constructs a client connection with the specified server address and client authentication options.
 95    /// All other properties use the <see cref="ClientConnectionOptions" /> defaults.</summary>
 96    /// <param name="serverAddress">The connection's server address.</param>
 97    /// <param name="clientAuthenticationOptions">The SSL client authentication options. When not <see langword="null"
 98    /// />, <see cref="ConnectAsync(CancellationToken)" /> will either establish a secure connection or fail.</param>
 99    /// <param name="duplexClientTransport">The duplex client transport. <see langword="null" /> is equivalent to <see
 100    /// cref="IDuplexClientTransport.Default" />.</param>
 101    /// <param name="multiplexedClientTransport">The multiplexed client transport. <see langword="null" /> is equivalent
 102    /// to <see cref="IMultiplexedClientTransport.Default" />.</param>
 103    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance" />.
 104    /// </param>
 105    public ClientConnection(
 106        ServerAddress serverAddress,
 107        SslClientAuthenticationOptions? clientAuthenticationOptions = null,
 108        IDuplexClientTransport? duplexClientTransport = null,
 109        IMultiplexedClientTransport? multiplexedClientTransport = null,
 110        ILogger? logger = null)
 46111        : this(
 46112            new ClientConnectionOptions
 46113            {
 46114                ClientAuthenticationOptions = clientAuthenticationOptions,
 46115                ServerAddress = serverAddress
 46116            },
 46117            duplexClientTransport,
 46118            multiplexedClientTransport,
 46119            logger)
 46120    {
 46121    }
 122
 123    /// <summary>Constructs a client connection with the specified server address URI and client authentication options.
 124    /// All other properties use the <see cref="ClientConnectionOptions" /> defaults.</summary>
 125    /// <param name="serverAddressUri">The connection's server address URI.</param>
 126    /// <param name="clientAuthenticationOptions">The SSL client authentication options. When not <see langword="null"
 127    /// />, <see cref="ConnectAsync(CancellationToken)" /> will either establish a secure connection or fail.</param>
 128    /// <param name="duplexClientTransport">The duplex client transport. <see langword="null" /> is equivalent to <see
 129    /// cref="IDuplexClientTransport.Default" />.</param>
 130    /// <param name="multiplexedClientTransport">The multiplexed client transport. <see langword="null" /> is equivalent
 131    /// to <see cref="IMultiplexedClientTransport.Default" />.</param>
 132    /// <param name="logger">The logger. <see langword="null" /> is equivalent to <see cref="NullLogger.Instance" />.
 133    /// </param>
 134    public ClientConnection(
 135        Uri serverAddressUri,
 136        SslClientAuthenticationOptions? clientAuthenticationOptions = null,
 137        IDuplexClientTransport? duplexClientTransport = null,
 138        IMultiplexedClientTransport? multiplexedClientTransport = null,
 139        ILogger? logger = null)
 4140        : this(
 4141            new ServerAddress(serverAddressUri),
 4142            clientAuthenticationOptions,
 4143            duplexClientTransport,
 4144            multiplexedClientTransport,
 4145            logger)
 4146    {
 4147    }
 148
 149    /// <summary>Establishes the connection.</summary>
 150    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 151    /// <returns>A task that provides the <see cref="TransportConnectionInformation" /> of the transport connection,
 152    /// once this connection is established.</returns>
 153    /// <exception cref="InvalidOperationException">Thrown if this client connection is shut down or shutting down.
 154    /// </exception>
 155    /// <exception cref="ObjectDisposedException">Thrown if this client connection is disposed.</exception>
 156    /// <remarks><para>This method can be called multiple times and concurrently. If the connection is not established,
 157    /// it will be connected or reconnected.</para>
 158    /// <para>The returned task can also complete with one of the following exceptions:</para>
 159    /// <list type="bullet">
 160    /// <item><description><see cref="AuthenticationException" /> if authentication failed.</description></item>
 161    /// <item><description><see cref="IceRpcException" /> if the connection establishment failed.</description></item>
 162    /// <item><description><see cref="OperationCanceledException" /> if cancellation was requested through the
 163    /// cancellation token.</description></item>
 164    /// <item><description><see cref="TimeoutException" /> if this connection attempt or a previous attempt exceeded
 165    /// <see cref="ClientConnectionOptions.ConnectTimeout" />.</description></item>
 166    /// </list>
 167    /// </remarks>
 168    public Task<TransportConnectionInformation> ConnectAsync(CancellationToken cancellationToken = default)
 59169    {
 170        Task<TransportConnectionInformation> connectTask;
 171
 172        lock (_mutex)
 59173        {
 59174            ObjectDisposedException.ThrowIf(_disposeTask is not null, this);
 59175            if (_shutdownTask is not null)
 0176            {
 0177                throw new InvalidOperationException("Cannot connect a client connection after shutting it down.");
 178            }
 179
 59180            if (_activeConnection is not null)
 1181            {
 1182                return Task.FromResult(_activeConnection.Value.ConnectionInformation);
 183            }
 184
 58185            if (_pendingConnection is null)
 58186            {
 58187                IProtocolConnection newConnection = _clientProtocolConnectionFactory.CreateConnection(_serverAddress);
 58188                _detachedConnectionCount++;
 58189                connectTask = CreateConnectTask(newConnection, cancellationToken);
 58190                _pendingConnection = (newConnection, connectTask);
 58191            }
 192            else
 0193            {
 0194                connectTask = _pendingConnection.Value.ConnectTask.WaitAsync(cancellationToken);
 0195            }
 58196        }
 197
 58198        return PerformConnectAsync();
 199
 200        async Task<TransportConnectionInformation> PerformConnectAsync()
 58201        {
 202            try
 58203            {
 58204                return await connectTask.ConfigureAwait(false);
 205            }
 3206            catch (OperationCanceledException)
 3207            {
 208                // Canceled via the cancellation token given to ConnectAsync, but not necessarily this ConnectAsync
 209                // call.
 210
 3211                cancellationToken.ThrowIfCancellationRequested();
 212
 0213                throw new IceRpcException(
 0214                    IceRpcError.ConnectionAborted,
 0215                    "The connection establishment was canceled by another concurrent attempt.");
 216            }
 37217        }
 59218    }
 219
 220    /// <summary>Releases all resources allocated by the connection. The connection disposes all the underlying
 221    /// connections it created.</summary>
 222    /// <returns>A value task that completes when the disposal of all the underlying connections has
 223    /// completed.</returns>
 224    /// <remarks>The disposal of an underlying connection aborts invocations, cancels dispatches and disposes the
 225    /// underlying transport connection without waiting for the peer. To wait for invocations and dispatches to
 226    /// complete, call <see cref="ShutdownAsync" /> first. If the configured dispatcher does not complete promptly when
 227    /// its cancellation token is canceled, the disposal can hang.</remarks>
 228    public ValueTask DisposeAsync()
 98229    {
 230        lock (_mutex)
 98231        {
 98232            if (_disposeTask is null)
 91233            {
 91234                _shutdownTask ??= Task.CompletedTask;
 91235                if (_detachedConnectionCount == 0)
 86236                {
 86237                    _ = _detachedConnectionsTcs.TrySetResult();
 86238                }
 239
 91240                _disposeTask = PerformDisposeAsync();
 91241            }
 98242        }
 98243        return new(_disposeTask);
 244
 245        async Task PerformDisposeAsync()
 91246        {
 91247            await Task.Yield(); // Exit mutex lock
 248
 91249            _disposedCts.Cancel();
 250
 251            // Wait for shutdown before disposing connections.
 252            try
 91253            {
 91254                await _shutdownTask.ConfigureAwait(false);
 89255            }
 2256            catch
 2257            {
 258                // ignore exceptions.
 2259            }
 260
 261            // Since a pending connection is "detached", it's disposed via the connectTask, not directly by this method.
 91262            if (_activeConnection is not null)
 58263            {
 58264                await _activeConnection.Value.Connection.DisposeAsync().ConfigureAwait(false);
 58265            }
 266
 91267            await _detachedConnectionsTcs.Task.ConfigureAwait(false);
 268
 91269            _disposedCts.Dispose();
 91270        }
 98271    }
 272
 273    /// <summary>Sends an outgoing request and returns the corresponding incoming response.</summary>
 274    /// <param name="request">The outgoing request being sent.</param>
 275    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 276    /// <returns>The corresponding <see cref="IncomingResponse" />.</returns>
 277    /// <exception cref="InvalidOperationException">Thrown if none of the request's server addresses matches this
 278    /// connection's server address.</exception>
 279    /// <exception cref="IceRpcException">Thrown with error <see cref="IceRpcError.InvocationRefused" /> if this client
 280    /// connection is shutdown.</exception>
 281    /// <exception cref="ObjectDisposedException">Thrown if this client connection is disposed.</exception>
 282    /// <remarks>If the connection is not established, it will be connected or reconnected.</remarks>
 283    public Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken = default)
 46284    {
 46285        if (request.Features.Get<IServerAddressFeature>() is IServerAddressFeature serverAddressFeature)
 0286        {
 0287            if (serverAddressFeature.ServerAddress is ServerAddress mainServerAddress)
 0288            {
 0289                CheckRequestServerAddresses(mainServerAddress, serverAddressFeature.AltServerAddresses);
 0290            }
 0291        }
 46292        else if (request.ServiceAddress.ServerAddress is ServerAddress mainServerAddress)
 17293        {
 17294            CheckRequestServerAddresses(mainServerAddress, request.ServiceAddress.AltServerAddresses);
 11295        }
 296        // It's ok if the request has no server address at all.
 297
 40298        IProtocolConnection? activeConnection = null;
 299
 300        lock (_mutex)
 40301        {
 40302            ObjectDisposedException.ThrowIf(_disposeTask is not null, this);
 303
 40304            if (_shutdownTask is not null)
 0305            {
 0306                throw new IceRpcException(IceRpcError.InvocationRefused, "The client connection was shut down.");
 307            }
 308
 40309            activeConnection = _activeConnection?.Connection;
 40310        }
 311
 40312        return PerformInvokeAsync(activeConnection);
 313
 314        void CheckRequestServerAddresses(
 315            ServerAddress mainServerAddress,
 316            ImmutableList<ServerAddress> altServerAddresses)
 17317        {
 17318            if (ServerAddressComparer.OptionalTransport.Equals(mainServerAddress, _serverAddress))
 10319            {
 10320                return;
 321            }
 322
 22323            foreach (ServerAddress serverAddress in altServerAddresses)
 1324            {
 1325                if (ServerAddressComparer.OptionalTransport.Equals(serverAddress, _serverAddress))
 1326                {
 1327                    return;
 328                }
 0329            }
 330
 6331            throw new InvalidOperationException(
 6332                $"None of the request's server addresses matches this connection's server address: {_serverAddress}");
 11333        }
 334
 335        async Task<IncomingResponse> PerformInvokeAsync(IProtocolConnection? connection)
 40336        {
 337            // When InvokeAsync throws an IceRpcException(InvocationRefused) we retry unless the client connection is
 338            // being shutdown or disposed.
 40339            while (true)
 40340            {
 40341                connection ??= await GetActiveConnectionAsync(cancellationToken).ConfigureAwait(false);
 342
 343                try
 40344                {
 40345                    return await connection.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 346                }
 0347                catch (ObjectDisposedException)
 0348                {
 349                    // This can occasionally happen if we find a connection that was just closed and then automatically
 350                    // disposed by this client connection.
 0351                }
 2352                catch (IceRpcException exception) when (exception.IceRpcError == IceRpcError.InvocationRefused)
 0353                {
 354                    // The connection is refusing new invocations.
 0355                }
 2356                catch (IceRpcException exception) when (exception.IceRpcError == IceRpcError.OperationAborted)
 2357                {
 358                    lock (_mutex)
 2359                    {
 2360                        if (_disposeTask is null)
 0361                        {
 0362                            throw new IceRpcException(
 0363                                IceRpcError.ConnectionAborted,
 0364                                "The underlying connection was disposed while the invocation was in progress.");
 365                        }
 366                        else
 2367                        {
 2368                            throw;
 369                        }
 370                    }
 371                }
 372
 373                // Make sure connection is no longer in _activeConnection before we retry.
 0374                _ = RemoveFromActiveAsync(connection);
 0375                connection = null;
 0376            }
 36377        }
 40378    }
 379
 380    /// <summary>Gracefully shuts down the connection. The shutdown waits for pending invocations and dispatches to
 381    /// complete.</summary>
 382    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 383    /// <returns>A task that completes once the shutdown is complete.</returns>
 384    /// <exception cref="InvalidOperationException">Thrown if this connection is already shut down or shutting down.
 385    /// </exception>
 386    /// <exception cref="ObjectDisposedException">Thrown if this connection is disposed.</exception>
 387    /// <remarks><para>The returned task can also complete with one of the following exceptions:</para>
 388    /// <list type="bullet">
 389    /// <item><description><see cref="IceRpcException" /> if the connection shutdown failed.</description></item>
 390    /// <item><description><see cref="OperationCanceledException" /> if cancellation was requested through the
 391    /// cancellation token.</description></item>
 392    /// <item><description><see cref="TimeoutException" /> if this shutdown attempt or a previous attempt exceeded <see
 393    /// cref="ClientConnectionOptions.ShutdownTimeout" />.</description></item>
 394    /// </list>
 395    /// </remarks>
 396    public Task ShutdownAsync(CancellationToken cancellationToken = default)
 17397    {
 398        lock (_mutex)
 17399        {
 17400            ObjectDisposedException.ThrowIf(_disposeTask is not null, this);
 17401            if (_shutdownTask is not null)
 0402            {
 0403                throw new InvalidOperationException("The client connection is already shut down or shutting down.");
 404            }
 405
 17406            if (_detachedConnectionCount == 0)
 17407            {
 17408                _ = _detachedConnectionsTcs.TrySetResult();
 17409            }
 410
 17411            _shutdownTask = PerformShutdownAsync();
 17412            return _shutdownTask;
 413        }
 414
 415        async Task PerformShutdownAsync()
 17416        {
 17417            await Task.Yield(); // exit mutex lock
 418
 17419            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposedCts.Token);
 17420            cts.CancelAfter(_shutdownTimeout);
 421
 422            // Since a pending connection is "detached", it's shutdown and disposed via the connectTask, not directly by
 423            // this method.
 424            try
 17425            {
 17426                if (_activeConnection is not null)
 15427                {
 15428                    await _activeConnection.Value.Connection.ShutdownAsync(cts.Token).ConfigureAwait(false);
 13429                }
 430
 15431                await _detachedConnectionsTcs.Task.WaitAsync(cts.Token).ConfigureAwait(false);
 15432            }
 2433            catch (OperationCanceledException)
 2434            {
 2435                cancellationToken.ThrowIfCancellationRequested();
 436
 1437                if (_disposedCts.IsCancellationRequested)
 0438                {
 0439                    throw new IceRpcException(
 0440                        IceRpcError.OperationAborted,
 0441                        "The shutdown was aborted because the client connection was disposed.");
 442                }
 443                else
 1444                {
 1445                    throw new TimeoutException(
 1446                        $"The client connection shut down timed out after {_shutdownTimeout.TotalSeconds} s.");
 447                }
 448            }
 0449            catch
 0450            {
 451                // ignore other shutdown exception
 0452            }
 15453        }
 17454    }
 455
 456    /// <summary>Creates the connection establishment task for a pending connection.</summary>
 457    /// <param name="connection">The new pending connection to connect.</param>
 458    /// <param name="cancellationToken">The cancellation token that can cancel this task.</param>
 459    /// <returns>A task that completes successfully when the connection is connected.</returns>
 460    private async Task<TransportConnectionInformation> CreateConnectTask(
 461        IProtocolConnection connection,
 462        CancellationToken cancellationToken)
 84463    {
 84464        await Task.Yield(); // exit mutex lock
 465
 466        // This task "owns" a detachedConnectionCount and as a result _disposedCts can't be disposed.
 84467        using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposedCts.Token);
 84468        cts.CancelAfter(_connectTimeout);
 469
 470        TransportConnectionInformation connectionInformation;
 471        Task shutdownRequested;
 84472        Task? connectTask = null;
 473
 474        try
 84475        {
 476            try
 84477            {
 84478                (connectionInformation, shutdownRequested) = await connection.ConnectAsync(cts.Token)
 84479                    .ConfigureAwait(false);
 63480            }
 5481            catch (OperationCanceledException)
 5482            {
 5483                cancellationToken.ThrowIfCancellationRequested();
 484
 2485                if (_disposedCts.IsCancellationRequested)
 1486                {
 1487                    throw new IceRpcException(
 1488                        IceRpcError.OperationAborted,
 1489                        "The connection establishment was aborted because the client connection was disposed.");
 490                }
 491                else
 1492                {
 1493                    throw new TimeoutException(
 1494                        $"The connection establishment timed out after {_connectTimeout.TotalSeconds} s.");
 495                }
 496            }
 63497        }
 21498        catch
 21499        {
 500            lock (_mutex)
 21501            {
 21502                Debug.Assert(_pendingConnection is not null && _pendingConnection.Value.Connection == connection);
 21503                Debug.Assert(_activeConnection is null);
 504
 505                // connectTask is executing this method and about to throw.
 21506                connectTask = _pendingConnection.Value.ConnectTask;
 21507                _pendingConnection = null;
 21508            }
 509
 21510            _ = DisposePendingConnectionAsync(connection, connectTask);
 21511            throw;
 512        }
 513
 514        lock (_mutex)
 63515        {
 63516            Debug.Assert(_pendingConnection is not null && _pendingConnection.Value.Connection == connection);
 63517            Debug.Assert(_activeConnection is null);
 518
 63519            if (_shutdownTask is null)
 63520            {
 521                // the connection is now "attached" in _activeConnection
 63522                _activeConnection = (connection, connectionInformation);
 63523                _detachedConnectionCount--;
 63524            }
 525            else
 0526            {
 0527                connectTask = _pendingConnection.Value.ConnectTask;
 0528            }
 63529            _pendingConnection = null;
 63530        }
 531
 63532        if (connectTask is null)
 63533        {
 63534            _ = ShutdownWhenRequestedAsync(connection, shutdownRequested);
 63535        }
 536        else
 0537        {
 538            // As soon as this method completes successfully, we shut down then dispose the connection.
 0539            _ = DisposePendingConnectionAsync(connection, connectTask);
 0540        }
 63541        return connectionInformation;
 542
 543        async Task DisposePendingConnectionAsync(IProtocolConnection connection, Task connectTask)
 21544        {
 545            try
 21546            {
 21547                await connectTask.ConfigureAwait(false);
 548
 549                // Since we own a detachedConnectionCount, _disposedCts is not disposed.
 0550                using var cts = CancellationTokenSource.CreateLinkedTokenSource(_disposedCts.Token);
 0551                cts.CancelAfter(_shutdownTimeout);
 0552                await connection.ShutdownAsync(cts.Token).ConfigureAwait(false);
 0553            }
 21554            catch
 21555            {
 556                // Observe and ignore exceptions.
 21557            }
 558
 21559            await connection.DisposeAsync().ConfigureAwait(false);
 560
 561            lock (_mutex)
 21562            {
 21563                if (--_detachedConnectionCount == 0 && _shutdownTask is not null)
 5564                {
 5565                    _detachedConnectionsTcs.SetResult();
 5566                }
 21567            }
 21568        }
 569
 570        async Task ShutdownWhenRequestedAsync(IProtocolConnection connection, Task shutdownRequested)
 63571        {
 63572            await shutdownRequested.ConfigureAwait(false);
 16573            await RemoveFromActiveAsync(connection).ConfigureAwait(false);
 16574        }
 63575    }
 576
 577    /// <summary>Removes the connection from _activeConnection, and when successful, shuts down and disposes this
 578    /// connection.</summary>
 579    /// <param name="connection">The connected connection to shutdown and dispose.</param>
 580    private Task RemoveFromActiveAsync(IProtocolConnection connection)
 16581    {
 582        lock (_mutex)
 16583        {
 16584            if (_shutdownTask is null && _activeConnection?.Connection == connection)
 5585            {
 5586                _activeConnection = null; // it's now our connection.
 5587                _detachedConnectionCount++;
 5588            }
 589            else
 11590            {
 591                // Another task owns this connection
 11592                return Task.CompletedTask;
 593            }
 5594        }
 595
 5596        return ShutdownAndDisposeConnectionAsync();
 597
 598        async Task ShutdownAndDisposeConnectionAsync()
 5599        {
 600            // _disposedCts is not disposed since we own a detachedConnectionCount
 5601            using var cts = CancellationTokenSource.CreateLinkedTokenSource(_disposedCts.Token);
 5602            cts.CancelAfter(_shutdownTimeout);
 603
 604            try
 5605            {
 5606                await connection.ShutdownAsync(cts.Token).ConfigureAwait(false);
 4607            }
 1608            catch
 1609            {
 610                // Ignore connection shutdown failures
 1611            }
 612
 5613            await connection.DisposeAsync().ConfigureAwait(false);
 614
 615            lock (_mutex)
 5616            {
 5617                if (--_detachedConnectionCount == 0 && _shutdownTask is not null)
 0618                {
 0619                    _detachedConnectionsTcs.SetResult();
 0620                }
 5621            }
 5622        }
 16623    }
 624
 625    /// <summary>Gets an active connection, by creating and connecting (if necessary) a new protocol connection.
 626    /// </summary>
 627    /// <param name="cancellationToken">The cancellation token of the invocation calling this method.</param>
 628    /// <returns>A connected connection.</returns>
 629    /// <remarks>This method is called exclusively by <see cref="InvokeAsync" />.</remarks>
 630    private ValueTask<IProtocolConnection> GetActiveConnectionAsync(CancellationToken cancellationToken)
 26631    {
 632        (IProtocolConnection Connection, Task<TransportConnectionInformation> ConnectTask) pendingConnectionValue;
 633
 634        lock (_mutex)
 26635        {
 26636            if (_disposeTask is not null)
 0637            {
 0638                throw new IceRpcException(IceRpcError.OperationAborted, "The client connection was disposed.");
 639            }
 26640            if (_shutdownTask is not null)
 0641            {
 0642                throw new IceRpcException(IceRpcError.InvocationRefused, "The client connection was shut down.");
 643            }
 644
 26645            if (_activeConnection is not null)
 0646            {
 0647                return new(_activeConnection.Value.Connection);
 648            }
 649
 26650            if (_pendingConnection is null)
 26651            {
 26652                IProtocolConnection connection = _clientProtocolConnectionFactory.CreateConnection(_serverAddress);
 26653                _detachedConnectionCount++;
 654
 655                // We pass CancellationToken.None because the invocation cancellation should not cancel the connection
 656                // establishment.
 26657                Task<TransportConnectionInformation> connectTask =
 26658                    CreateConnectTask(connection, CancellationToken.None);
 26659                _pendingConnection = (connection, connectTask);
 26660            }
 26661            pendingConnectionValue = _pendingConnection.Value;
 26662        }
 663
 26664        return PerformGetActiveConnectionAsync();
 665
 666        async ValueTask<IProtocolConnection> PerformGetActiveConnectionAsync()
 26667        {
 668            // ConnectTask itself takes care of scheduling its exception observation when it fails.
 669            try
 26670            {
 26671                _ = await pendingConnectionValue.ConnectTask.WaitAsync(cancellationToken).ConfigureAwait(false);
 26672            }
 0673            catch (OperationCanceledException)
 0674            {
 0675                cancellationToken.ThrowIfCancellationRequested();
 676
 677                // Canceled by the cancellation token given to ClientConnection.ConnectAsync.
 0678                throw new IceRpcException(
 0679                    IceRpcError.ConnectionAborted,
 0680                    "The connection establishment was canceled by another concurrent attempt.");
 681            }
 26682            return pendingConnectionValue.Connection;
 26683        }
 26684    }
 685}