< Summary

Information
Class: IceRpc.ClientConnection
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/ClientConnection.cs
Tag: 2300_35243572715
Line coverage
83%
Covered lines: 300
Uncovered lines: 61
Coverable lines: 361
Total lines: 696
Line coverage: 83.1%
Branch coverage
76%
Covered branches: 72
Total branches: 94
Branch coverage: 76.5%
Method coverage
100%
Covered methods: 19
Fully covered methods: 8
Total methods: 19
Method coverage: 100%
Full method coverage: 42.1%

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()83.33%7675%
CreateConnectTask()66.66%121288.88%
DisposePendingConnectionAsync()100%4477.77%
ShutdownWhenRequestedAsync()100%11100%
RemoveFromActiveAsync(...)83.33%66100%
ShutdownAndDisposeConnectionAsync()75%44100%
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
 9233    private readonly TaskCompletionSource _detachedConnectionsTcs =
 9234        new(TaskCreationOptions.RunContinuationsAsynchronously);
 35
 36    // A cancellation token source that is canceled when DisposeAsync is called.
 9237    private readonly CancellationTokenSource _disposedCts = new();
 38    private Task? _disposeTask;
 39
 9240    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>
 9259    public ClientConnection(
 9260        ClientConnectionOptions options,
 9261        IDuplexClientTransport? duplexClientTransport = null,
 9262        IMultiplexedClientTransport? multiplexedClientTransport = null,
 9263        ILogger? logger = null)
 9264    {
 9265        _connectTimeout = options.ConnectTimeout;
 9266        _shutdownTimeout = options.ShutdownTimeout;
 67
 9268        duplexClientTransport ??= IDuplexClientTransport.Default;
 9269        multiplexedClientTransport ??= IMultiplexedClientTransport.Default;
 70
 9271        _serverAddress = options.ServerAddress ??
 9272            throw new ArgumentException(
 9273                $"{nameof(ClientConnectionOptions.ServerAddress)} is not set",
 9274                nameof(options));
 75
 9276        if (_serverAddress.Transport is null)
 5177        {
 5178            _serverAddress = _serverAddress with
 5179            {
 5180                Transport = _serverAddress.Protocol == Protocol.Ice ?
 5181                    duplexClientTransport.DefaultName : multiplexedClientTransport.DefaultName
 5182            };
 5183        }
 84
 9285        _clientProtocolConnectionFactory = new ClientProtocolConnectionFactory(
 9286            options,
 9287            options.ConnectTimeout,
 9288            options.ClientAuthenticationOptions,
 9289            duplexClientTransport,
 9290            multiplexedClientTransport,
 9291            logger);
 9292    }
 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 when this client connection is shut down or shutting down.
 154    /// </exception>
 155    /// <exception cref="ObjectDisposedException">Thrown when 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()
 99229    {
 230        lock (_mutex)
 99231        {
 99232            if (_disposeTask is null)
 92233            {
 92234                _shutdownTask ??= Task.CompletedTask;
 92235                if (_detachedConnectionCount == 0)
 87236                {
 87237                    _ = _detachedConnectionsTcs.TrySetResult();
 87238                }
 239
 92240                _disposeTask = PerformDisposeAsync();
 92241            }
 99242        }
 99243        return new(_disposeTask);
 244
 245        async Task PerformDisposeAsync()
 92246        {
 92247            await Task.Yield(); // Exit mutex lock
 248
 92249            _disposedCts.Cancel();
 250
 251            // Wait for shutdown before disposing connections.
 252            try
 92253            {
 92254                await _shutdownTask.ConfigureAwait(false);
 90255            }
 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.
 92262            if (_activeConnection is not null)
 55263            {
 55264                await _activeConnection.Value.Connection.DisposeAsync().ConfigureAwait(false);
 55265            }
 266
 92267            await _detachedConnectionsTcs.Task.ConfigureAwait(false);
 268
 92269            _disposedCts.Dispose();
 92270        }
 99271    }
 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 when 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" /> when this
 280    /// client connection is shut down.</exception>
 281    /// <exception cref="ObjectDisposedException">Thrown when 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 when this connection is already shut down or shutting down.
 385    /// </exception>
 386    /// <exception cref="ObjectDisposedException">Thrown when 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" /> with error <see cref="IceRpcError.OperationAborted" /> if this
 390    /// connection is disposed while being shut down.</description></item>
 391    /// <item><description><see cref="OperationCanceledException" /> if cancellation was requested through the
 392    /// cancellation token.</description></item>
 393    /// <item><description><see cref="TimeoutException" /> if this shutdown attempt or a previous attempt exceeded <see
 394    /// cref="ClientConnectionOptions.ShutdownTimeout" />.</description></item>
 395    /// </list>
 396    /// </remarks>
 397    public Task ShutdownAsync(CancellationToken cancellationToken = default)
 17398    {
 399        lock (_mutex)
 17400        {
 17401            ObjectDisposedException.ThrowIf(_disposeTask is not null, this);
 17402            if (_shutdownTask is not null)
 0403            {
 0404                throw new InvalidOperationException("The client connection is already shut down or shutting down.");
 405            }
 406
 17407            if (_detachedConnectionCount == 0)
 17408            {
 17409                _ = _detachedConnectionsTcs.TrySetResult();
 17410            }
 411
 17412            _shutdownTask = PerformShutdownAsync();
 17413            return _shutdownTask;
 414        }
 415
 416        async Task PerformShutdownAsync()
 17417        {
 17418            await Task.Yield(); // exit mutex lock
 419
 17420            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposedCts.Token);
 17421            cts.CancelAfter(_shutdownTimeout);
 422
 423            try
 17424            {
 425                // Since a pending connection is "detached", it's shutdown and disposed via the connectTask, not
 426                // directly by this method.
 427                try
 17428                {
 17429                    Task shutdownTask = _activeConnection is null ?
 17430                        Task.CompletedTask :
 17431                        _activeConnection.Value.Connection.ShutdownAsync(cts.Token);
 432
 17433                    await Task.WhenAll(shutdownTask, _detachedConnectionsTcs.Task.WaitAsync(cts.Token))
 17434                        .ConfigureAwait(false);
 15435                }
 2436                catch (OperationCanceledException)
 2437                {
 2438                    throw;
 439                }
 0440                catch
 0441                {
 442                    // Ignore other connection shutdown failures.
 443
 444                    // Throw OperationCanceledException if this WhenAll exception is hiding an OCE.
 0445                    cts.Token.ThrowIfCancellationRequested();
 0446                }
 15447            }
 2448            catch (OperationCanceledException)
 2449            {
 2450                cancellationToken.ThrowIfCancellationRequested();
 451
 1452                if (_disposedCts.IsCancellationRequested)
 0453                {
 0454                    throw new IceRpcException(
 0455                        IceRpcError.OperationAborted,
 0456                        "The shutdown was aborted because the client connection was disposed.");
 457                }
 458                else
 1459                {
 1460                    throw new TimeoutException(
 1461                        $"The client connection shut down timed out after {_shutdownTimeout.TotalSeconds} s.");
 462                }
 463            }
 15464        }
 17465    }
 466
 467    /// <summary>Creates the connection establishment task for a pending connection.</summary>
 468    /// <param name="connection">The new pending connection to connect.</param>
 469    /// <param name="cancellationToken">The cancellation token that can cancel this task.</param>
 470    /// <returns>A task that completes successfully when the connection is connected.</returns>
 471    private async Task<TransportConnectionInformation> CreateConnectTask(
 472        IProtocolConnection connection,
 473        CancellationToken cancellationToken)
 84474    {
 84475        await Task.Yield(); // exit mutex lock
 476
 477        // This task "owns" a detachedConnectionCount and as a result _disposedCts can't be disposed.
 84478        using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposedCts.Token);
 84479        cts.CancelAfter(_connectTimeout);
 480
 481        TransportConnectionInformation connectionInformation;
 482        Task shutdownRequested;
 84483        Task? connectTask = null;
 484
 485        try
 84486        {
 487            try
 84488            {
 84489                (connectionInformation, shutdownRequested) = await connection.ConnectAsync(cts.Token)
 84490                    .ConfigureAwait(false);
 63491            }
 5492            catch (OperationCanceledException)
 5493            {
 5494                cancellationToken.ThrowIfCancellationRequested();
 495
 2496                if (_disposedCts.IsCancellationRequested)
 1497                {
 1498                    throw new IceRpcException(
 1499                        IceRpcError.OperationAborted,
 1500                        "The connection establishment was aborted because the client connection was disposed.");
 501                }
 502                else
 1503                {
 1504                    throw new TimeoutException(
 1505                        $"The connection establishment timed out after {_connectTimeout.TotalSeconds} s.");
 506                }
 507            }
 63508        }
 21509        catch
 21510        {
 511            lock (_mutex)
 21512            {
 21513                Debug.Assert(_pendingConnection is not null && _pendingConnection.Value.Connection == connection);
 21514                Debug.Assert(_activeConnection is null);
 515
 516                // connectTask is executing this method and about to throw.
 21517                connectTask = _pendingConnection.Value.ConnectTask;
 21518                _pendingConnection = null;
 21519            }
 520
 21521            _ = DisposePendingConnectionAsync(connection, connectTask);
 21522            throw;
 523        }
 524
 525        lock (_mutex)
 63526        {
 63527            Debug.Assert(_pendingConnection is not null && _pendingConnection.Value.Connection == connection);
 63528            Debug.Assert(_activeConnection is null);
 529
 63530            if (_shutdownTask is null)
 63531            {
 532                // the connection is now "attached" in _activeConnection
 63533                _activeConnection = (connection, connectionInformation);
 63534                _detachedConnectionCount--;
 63535            }
 536            else
 0537            {
 0538                connectTask = _pendingConnection.Value.ConnectTask;
 0539            }
 63540            _pendingConnection = null;
 63541        }
 542
 63543        if (connectTask is null)
 63544        {
 63545            _ = ShutdownWhenRequestedAsync(connection, shutdownRequested);
 63546        }
 547        else
 0548        {
 549            // As soon as this method completes successfully, we shut down then dispose the connection.
 0550            _ = DisposePendingConnectionAsync(connection, connectTask);
 0551        }
 63552        return connectionInformation;
 553
 554        async Task DisposePendingConnectionAsync(IProtocolConnection connection, Task connectTask)
 21555        {
 556            try
 21557            {
 21558                await connectTask.ConfigureAwait(false);
 559
 560                // Since we own a detachedConnectionCount, _disposedCts is not disposed.
 0561                using var cts = CancellationTokenSource.CreateLinkedTokenSource(_disposedCts.Token);
 0562                cts.CancelAfter(_shutdownTimeout);
 0563                await connection.ShutdownAsync(cts.Token).ConfigureAwait(false);
 0564            }
 21565            catch
 21566            {
 567                // Observe and ignore exceptions.
 21568            }
 569
 21570            await connection.DisposeAsync().ConfigureAwait(false);
 571
 572            lock (_mutex)
 21573            {
 21574                if (--_detachedConnectionCount == 0 && _shutdownTask is not null)
 3575                {
 3576                    _detachedConnectionsTcs.SetResult();
 3577                }
 21578            }
 21579        }
 580
 581        async Task ShutdownWhenRequestedAsync(IProtocolConnection connection, Task shutdownRequested)
 63582        {
 63583            await shutdownRequested.ConfigureAwait(false);
 19584            await RemoveFromActiveAsync(connection).ConfigureAwait(false);
 19585        }
 63586    }
 587
 588    /// <summary>Removes the connection from _activeConnection, and when successful, shuts down and disposes this
 589    /// connection.</summary>
 590    /// <param name="connection">The connected connection to shut down and dispose.</param>
 591    private Task RemoveFromActiveAsync(IProtocolConnection connection)
 19592    {
 593        lock (_mutex)
 19594        {
 19595            if (_shutdownTask is null && _activeConnection?.Connection == connection)
 8596            {
 8597                _activeConnection = null; // it's now our connection.
 8598                _detachedConnectionCount++;
 8599            }
 600            else
 11601            {
 602                // Another task owns this connection
 11603                return Task.CompletedTask;
 604            }
 8605        }
 606
 8607        return ShutdownAndDisposeConnectionAsync();
 608
 609        async Task ShutdownAndDisposeConnectionAsync()
 8610        {
 611            // _disposedCts is not disposed since we own a detachedConnectionCount
 8612            using var cts = CancellationTokenSource.CreateLinkedTokenSource(_disposedCts.Token);
 8613            cts.CancelAfter(_shutdownTimeout);
 614
 615            try
 8616            {
 8617                await connection.ShutdownAsync(cts.Token).ConfigureAwait(false);
 4618            }
 4619            catch
 4620            {
 621                // Ignore connection shutdown failures
 4622            }
 623
 8624            await connection.DisposeAsync().ConfigureAwait(false);
 625
 626            lock (_mutex)
 8627            {
 8628                if (--_detachedConnectionCount == 0 && _shutdownTask is not null)
 2629                {
 2630                    _detachedConnectionsTcs.SetResult();
 2631                }
 8632            }
 8633        }
 19634    }
 635
 636    /// <summary>Gets an active connection, by creating and connecting (if necessary) a new protocol connection.
 637    /// </summary>
 638    /// <param name="cancellationToken">The cancellation token of the invocation calling this method.</param>
 639    /// <returns>A connected connection.</returns>
 640    /// <remarks>This method is called exclusively by <see cref="InvokeAsync" />.</remarks>
 641    private ValueTask<IProtocolConnection> GetActiveConnectionAsync(CancellationToken cancellationToken)
 26642    {
 643        (IProtocolConnection Connection, Task<TransportConnectionInformation> ConnectTask) pendingConnectionValue;
 644
 645        lock (_mutex)
 26646        {
 26647            if (_disposeTask is not null)
 0648            {
 0649                throw new IceRpcException(IceRpcError.OperationAborted, "The client connection was disposed.");
 650            }
 26651            if (_shutdownTask is not null)
 0652            {
 0653                throw new IceRpcException(IceRpcError.InvocationRefused, "The client connection was shut down.");
 654            }
 655
 26656            if (_activeConnection is not null)
 0657            {
 0658                return new(_activeConnection.Value.Connection);
 659            }
 660
 26661            if (_pendingConnection is null)
 26662            {
 26663                IProtocolConnection connection = _clientProtocolConnectionFactory.CreateConnection(_serverAddress);
 26664                _detachedConnectionCount++;
 665
 666                // We pass CancellationToken.None because the invocation cancellation should not cancel the connection
 667                // establishment.
 26668                Task<TransportConnectionInformation> connectTask =
 26669                    CreateConnectTask(connection, CancellationToken.None);
 26670                _pendingConnection = (connection, connectTask);
 26671            }
 26672            pendingConnectionValue = _pendingConnection.Value;
 26673        }
 674
 26675        return PerformGetActiveConnectionAsync();
 676
 677        async ValueTask<IProtocolConnection> PerformGetActiveConnectionAsync()
 26678        {
 679            // ConnectTask itself takes care of scheduling its exception observation when it fails.
 680            try
 26681            {
 26682                _ = await pendingConnectionValue.ConnectTask.WaitAsync(cancellationToken).ConfigureAwait(false);
 26683            }
 0684            catch (OperationCanceledException)
 0685            {
 0686                cancellationToken.ThrowIfCancellationRequested();
 687
 688                // Canceled by the cancellation token given to ClientConnection.ConnectAsync.
 0689                throw new IceRpcException(
 0690                    IceRpcError.ConnectionAborted,
 0691                    "The connection establishment was canceled by another concurrent attempt.");
 692            }
 26693            return pendingConnectionValue.Connection;
 26694        }
 26695    }
 696}