< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 300
Coverable lines: 300
Total lines: 606
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 144
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)0%36360%
StartMonitoringNetworkChanges()0%10100%
.ctor(...)100%110%
Finalize()100%110%
Dispose()100%110%
GetConnectionKey(...)0%24240%
GetTelemetryServerAddress(...)0%12120%
SendAsyncCore(...)0%12120%
SendProxyConnectAsync(...)100%110%
SendAsync(...)0%16160%
SendAsyncMultiProxy(...)0%220%
Dispose()0%660%
SetCleaningTimer(...)0%220%
RemoveStalePools()0%660%
HeartBeat()0%220%
GetIdentityIfDefaultCredentialsUsed(...)0%220%
.ctor(...)100%110%
GetHashCode()0%220%
Equals(...)0%220%
Equals(...)0%10100%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Diagnostics.CodeAnalysis;
 8using System.Net.NetworkInformation;
 9using System.Runtime.ExceptionServices;
 10using System.Threading;
 11using System.Threading.Tasks;
 12
 13namespace System.Net.Http
 14{
 15    // General flow of requests through the various layers:
 16    //
 17    // (1) HttpConnectionPoolManager.SendAsync: Does proxy lookup
 18    // (2) HttpConnectionPoolManager.SendAsyncCore: Find or create connection pool
 19    // (3) HttpConnectionPool.SendAsync: Handle basic/digest request auth
 20    // (4) HttpConnectionPool.SendWithProxyAuthAsync: Handle basic/digest proxy auth
 21    // (5) HttpConnectionPool.SendWithRetryAsync: Retrieve connection from pool, or create new
 22    //                                            Also, handle retry for failures on connection reuse
 23    // (6) HttpConnection.SendAsync: Handle negotiate/ntlm connection auth
 24    // (7) HttpConnection.SendWithNtProxyAuthAsync: Handle negotiate/ntlm proxy auth
 25    // (8) HttpConnection.SendAsyncCore: Write request to connection and read response
 26    //                                   Also, handle cookie processing
 27    //
 28    // Redirect and decompression handling are done above HttpConnectionPoolManager,
 29    // in RedirectHandler and DecompressionHandler respectively.
 30
 31    /// <summary>Provides a set of connection pools, each for its own endpoint.</summary>
 32    internal sealed class HttpConnectionPoolManager : IDisposable
 33    {
 34        /// <summary>How frequently an operation should be initiated to clean out old pools and connections in those poo
 35        private readonly TimeSpan _cleanPoolTimeout;
 36        /// <summary>The pools, indexed by endpoint.</summary>
 37        private readonly ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool> _pools;
 38        /// <summary>Timer used to initiate cleaning of the pools.</summary>
 39        private readonly Timer? _cleaningTimer;
 40        /// <summary>Heart beat timer currently used for Http2 ping only. Not stopped by <see cref="Dispose"/>; it stops
 41        private readonly Timer? _heartBeatTimer;
 42
 43        private readonly HttpConnectionSettings _settings;
 44        private readonly IWebProxy? _proxy;
 45        private readonly ICredentials? _proxyCredentials;
 46
 47        private NetworkChangeCleanup? _networkChangeCleanup;
 48
 49        /// <summary>
 50        /// Keeps track of whether or not the cleanup timer is running. It helps us avoid the expensive
 51        /// <see cref="ConcurrentDictionary{TKey,TValue}.IsEmpty"/> call.
 52        /// </summary>
 53        private bool _timerIsRunning;
 54        /// <summary>Whether <see cref="Dispose"/> has been called.</summary>
 55        private bool _disposed;
 56        /// <summary>Object used to synchronize access to state in the pool.</summary>
 057        private object SyncObj => _pools;
 58
 59        /// <summary>Initializes the pools.</summary>
 060        public HttpConnectionPoolManager(HttpConnectionSettings settings)
 061        {
 062            _settings = settings;
 063            _pools = new ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>();
 64
 65            // As an optimization, we can sometimes avoid the overheads associated with
 66            // storing connections.  This is possible when we would immediately terminate
 67            // connections anyway due to either the idle timeout or the lifetime being
 68            // set to zero, as in that case the timeout effectively immediately expires.
 69            // However, we can only do such optimizations if we're not also tracking
 70            // connections per server, as we use data in the associated data structures
 71            // to do that tracking.
 72            // Additionally, we should not avoid storing connections if keep-alive ping is configured,
 73            // as the heartbeat timer is needed for ping functionality.
 074            bool avoidStoringConnections =
 075                settings._maxConnectionsPerServer == int.MaxValue &&
 076                (settings._pooledConnectionIdleTimeout == TimeSpan.Zero ||
 077                 settings._pooledConnectionLifetime == TimeSpan.Zero) &&
 078                settings._keepAlivePingDelay == Timeout.InfiniteTimeSpan;
 79
 80            // Start out with the timer not running, since we have no pools.
 81            // When it does run, run it with a frequency based on the idle timeout.
 082            if (!avoidStoringConnections)
 083            {
 084                if (settings._pooledConnectionIdleTimeout == Timeout.InfiniteTimeSpan)
 085                {
 86                    const int DefaultScavengeSeconds = 30;
 087                    _cleanPoolTimeout = TimeSpan.FromSeconds(DefaultScavengeSeconds);
 088                }
 89                else
 090                {
 91                    const int ScavengesPerIdle = 4;
 92                    const int MinScavengeSeconds = 1;
 093                    TimeSpan timerPeriod = settings._pooledConnectionIdleTimeout / ScavengesPerIdle;
 094                    _cleanPoolTimeout = timerPeriod.TotalSeconds >= MinScavengeSeconds ? timerPeriod : TimeSpan.FromSeco
 095                }
 96
 97                // The connection eviction callback is invoked from this timer. If one is set, make sure the timer
 98                // fires at least this often so eviction decisions happen on a predictable cadence, regardless of
 99                // how large (or infinite) the idle timeout is, which would otherwise drive the period alone.
 0100                if (settings._shouldEvictConnection is not null)
 0101                {
 102                    const int MaxEvictionIntervalSeconds = 5;
 0103                    if (_cleanPoolTimeout.TotalSeconds > MaxEvictionIntervalSeconds)
 0104                    {
 0105                        _cleanPoolTimeout = TimeSpan.FromSeconds(MaxEvictionIntervalSeconds);
 0106                    }
 0107                }
 108
 0109                using (ExecutionContext.SuppressFlow()) // Don't capture the current ExecutionContext and its AsyncLocal
 0110                {
 111                    // Create the timer.  Ensure the Timer has a weak reference to this manager; otherwise, it
 112                    // can introduce a cycle that keeps the HttpConnectionPoolManager rooted by the Timer
 113                    // implementation until the handler is Disposed (or indefinitely if it's not).
 0114                    var thisRef = new WeakReference<HttpConnectionPoolManager>(this);
 115
 0116                    _cleaningTimer = new Timer(static s =>
 0117                    {
 0118                        var wr = (WeakReference<HttpConnectionPoolManager>)s!;
 0119                        if (wr.TryGetTarget(out HttpConnectionPoolManager? thisRef))
 0120                        {
 0121                            thisRef.RemoveStalePools();
 0122                        }
 0123                    }, thisRef, Timeout.Infinite, Timeout.Infinite);
 124
 125
 126                    // For now heart beat is used only for ping functionality.
 0127                    if (_settings._keepAlivePingDelay != Timeout.InfiniteTimeSpan)
 0128                    {
 0129                        long heartBeatInterval = (long)Math.Max(1000, Math.Min(_settings._keepAlivePingDelay.TotalMillis
 130
 131                        // Unlike the cleaning timer, this one is deliberately not stopped by Dispose.
 132                        // Requests that were already in flight keep running after the handler is disposed,
 133                        // and they must keep sending keep alive PINGs to detect an unresponsive server.
 134                        // Instead, the timer stops itself once the manager has been disposed and has no
 135                        // connections left. If the manager becomes unreachable without ever being disposed,
 136                        // the timer becomes unreachable with it and is stopped by its own finalizer.
 0137                        _heartBeatTimer = new Timer(static state =>
 0138                        {
 0139                            var wr = (WeakReference<HttpConnectionPoolManager>)state!;
 0140                            if (wr.TryGetTarget(out HttpConnectionPoolManager? manager) &&
 0141                                !manager.HeartBeat() &&
 0142                                manager._disposed)
 0143                            {
 0144                                manager._heartBeatTimer?.Dispose();
 0145                            }
 0146                        }, thisRef, heartBeatInterval, heartBeatInterval);
 0147                    }
 0148                }
 0149            }
 150
 151            // Figure out proxy stuff.
 0152            if (settings._useProxy)
 0153            {
 0154                _proxy = settings._proxy ?? HttpClient.DefaultProxy;
 0155                if (_proxy != null)
 0156                {
 0157                    _proxyCredentials = _proxy.Credentials ?? settings._defaultProxyCredentials;
 0158                }
 0159            }
 0160        }
 161
 162        /// <summary>
 163        /// Starts monitoring for network changes. Upon a change, <see cref="HttpConnectionPool.OnNetworkChanged"/> will
 164        /// called for every <see cref="HttpConnectionPool"/> in the <see cref="HttpConnectionPoolManager"/>.
 165        /// </summary>
 166        public void StartMonitoringNetworkChanges()
 0167        {
 0168            if (_networkChangeCleanup != null)
 0169            {
 0170                return;
 171            }
 172
 173            // Monitor network changes to invalidate Alt-Svc headers.
 174            // A weak reference is used to avoid NetworkChange.NetworkAddressChanged keeping a non-disposed connection p
 175            NetworkAddressChangedEventHandler networkChangedDelegate;
 0176            { // scope to avoid closure if _networkChangeCleanup != null
 0177                var poolsRef = new WeakReference<ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>>(_pools);
 0178                networkChangedDelegate = delegate
 0179                {
 0180                    if (poolsRef.TryGetTarget(out ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>? pools))
 0181                    {
 0182                        foreach (HttpConnectionPool pool in pools.Values)
 0183                        {
 0184                            pool.OnNetworkChanged();
 0185                        }
 0186                    }
 0187                };
 0188            }
 189
 0190            var cleanup = new NetworkChangeCleanup(networkChangedDelegate);
 191
 0192            if (Interlocked.CompareExchange(ref _networkChangeCleanup, cleanup, null) != null)
 0193            {
 194                // We lost a race, another thread already started monitoring.
 0195                GC.SuppressFinalize(cleanup);
 0196                return;
 197            }
 198
 199            // RFC: https://tools.ietf.org/html/rfc7838#section-2.2
 200            //    When alternative services are used to send a client to the most
 201            //    optimal server, a change in network configuration can result in
 202            //    cached values becoming suboptimal.  Therefore, clients SHOULD remove
 203            //    from cache all alternative services that lack the "persist" flag with
 204            //    the value "1" when they detect such a change, when information about
 205            //    network state is available.
 206            try
 0207            {
 0208                using (ExecutionContext.SuppressFlow())
 0209                {
 0210                    NetworkChange.NetworkAddressChanged += networkChangedDelegate;
 0211                }
 0212            }
 0213            catch (NetworkInformationException e)
 0214            {
 0215                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Exception when subscribing to NetworkCh
 216
 217                // We can't monitor network changes, so technically "information
 218                // about network state is not available" and we can just keep
 219                // all Alt-Svc entries until their expiration time.
 220                //
 221                // keep the _networkChangeCleanup field assigned so we don't try again needlessly
 0222            }
 0223        }
 224
 225        private sealed class NetworkChangeCleanup : IDisposable
 226        {
 227            private readonly NetworkAddressChangedEventHandler _handler;
 228
 0229            public NetworkChangeCleanup(NetworkAddressChangedEventHandler handler)
 0230            {
 0231                _handler = handler;
 0232            }
 233
 234            // If user never disposes the HttpClient, use finalizer to remove from NetworkChange.NetworkAddressChanged.
 235            // _handler will be rooted in NetworkChange, so should be safe to use here.
 0236            ~NetworkChangeCleanup() => NetworkChange.NetworkAddressChanged -= _handler;
 237
 238            public void Dispose()
 0239            {
 0240                NetworkChange.NetworkAddressChanged -= _handler;
 0241                GC.SuppressFinalize(this);
 0242            }
 243        }
 244
 0245        public HttpConnectionSettings Settings => _settings;
 0246        public ICredentials? ProxyCredentials => _proxyCredentials;
 247
 248        private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? proxyUri, bool isProxyConnect)
 0249        {
 0250            Uri? uri = request.RequestUri;
 0251            Debug.Assert(uri != null);
 252
 0253            if (isProxyConnect)
 0254            {
 0255                Debug.Assert(uri == proxyUri);
 0256                return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, uri.IdnHost, uri.Port, null, proxyUri, Get
 257            }
 258
 0259            string? sslHostName = null;
 0260            if (HttpUtilities.IsSupportedSecureScheme(uri.Scheme))
 0261            {
 0262                string? hostHeader = request.Headers.Host;
 0263                if (hostHeader != null)
 0264                {
 0265                    sslHostName = HttpUtilities.ParseHostNameFromHeader(hostHeader);
 0266                }
 267                else
 0268                {
 269                    // No explicit Host header. Use host from uri.
 0270                    sslHostName = uri.IdnHost;
 0271                }
 0272            }
 273
 0274            string identity = GetIdentityIfDefaultCredentialsUsed(proxyUri != null ? _settings._defaultCredentialsUsedFo
 275
 0276            if (proxyUri != null)
 0277            {
 0278                Debug.Assert(HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme));
 0279                if (HttpUtilities.IsSocksScheme(proxyUri.Scheme))
 0280                {
 281                    // Socks proxy
 0282                    if (sslHostName != null)
 0283                    {
 0284                        return new HttpConnectionKey(HttpConnectionKind.SslSocksTunnel, uri.IdnHost, uri.Port, sslHostNa
 285                    }
 286                    else
 0287                    {
 0288                        return new HttpConnectionKey(HttpConnectionKind.SocksTunnel, uri.IdnHost, uri.Port, null, proxyU
 289                    }
 290                }
 0291                else if (sslHostName == null)
 0292                {
 293                    // Both non-secure WebSockets (WS) and cleartext HTTP/2 (h2c) need a CONNECT tunnel to the destinati
 294                    // because they can't be expressed using the absolute-form request line an HTTP proxy expects.
 295                    // h2c is only tunneled when HTTP/2 is required or preferred; requests that allow downgrading to
 296                    // HTTP/1.1 (RequestVersionOrLower) keep using the shared HTTP/1.1 proxy pool below.
 0297                    if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme) ||
 0298                        (request.Version.Major == 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
 0299                    {
 0300                        return new HttpConnectionKey(HttpConnectionKind.ProxyTunnel, uri.IdnHost, uri.Port, null, proxyU
 301                    }
 302                    else
 0303                    {
 304                        // Standard HTTP proxy usage for non-secure requests
 305                        // The destination host and port are ignored here, since these connections
 306                        // will be shared across any requests that use the proxy.
 0307                        return new HttpConnectionKey(HttpConnectionKind.Proxy, null, 0, null, proxyUri, identity);
 308                    }
 309                }
 310                else
 0311                {
 312                    // Tunnel SSL connection through proxy to the destination.
 0313                    return new HttpConnectionKey(HttpConnectionKind.SslProxyTunnel, uri.IdnHost, uri.Port, sslHostName, 
 314                }
 315            }
 0316            else if (sslHostName != null)
 0317            {
 0318                return new HttpConnectionKey(HttpConnectionKind.Https, uri.IdnHost, uri.Port, sslHostName, null, identit
 319            }
 320            else
 0321            {
 0322                return new HttpConnectionKey(HttpConnectionKind.Http, uri.IdnHost, uri.Port, null, null, identity);
 323            }
 0324        }
 325
 326        // Picks the value of the 'server.address' tag following rules specified in
 327        // https://github.com/open-telemetry/semantic-conventions/blob/728e5d1/docs/http/http-spans.md#http-client-span
 328        // When there is no proxy, we need to prioritize the contents of the Host header.
 329        private static string? GetTelemetryServerAddress(HttpRequestMessage request, HttpConnectionKey key)
 0330        {
 0331            if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled || GlobalHttpSettings.DiagnosticsHandler.EnableActiv
 0332            {
 0333                Uri? uri = request.RequestUri;
 0334                Debug.Assert(uri is not null);
 335
 0336                if (key.SslHostName is not null)
 0337                {
 0338                    return key.SslHostName;
 339                }
 340
 0341                if (key.ProxyUri is not null && key.Kind == HttpConnectionKind.Proxy)
 0342                {
 343                    // In case there is no tunnel, return the proxy address since the connection is shared.
 0344                    return key.ProxyUri.IdnHost;
 345                }
 346
 0347                string? hostHeader = request.Headers.Host;
 0348                return hostHeader is null ? uri.IdnHost : HttpUtilities.ParseHostNameFromHeader(hostHeader);
 349            }
 350
 0351            return null;
 0352        }
 353
 354        public ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, Uri? proxyUri, bool async, bool 
 0355        {
 0356            HttpConnectionKey key = GetConnectionKey(request, proxyUri, isProxyConnect);
 357
 0358            string? sslHostName = key.SslHostName;
 359
 0360            if (sslHostName is not null && request.IsConnectionPoolPartitioningBySniDisabled())
 0361            {
 362                // The request is using HTTPS, but has opted out of partitioning the connection pool by SNI.
 363                // The connection pool will be shared by requests to the same Uri Host, regardless of the Host header.
 364                // This enables requests where the Uri is set to an IP of shared infrastructure to share connections eve
 365                // This is a dangerous opt-in where the caller is responsible for ensuring that the server certificate i
 0366                key = new HttpConnectionKey(key.Kind, key.Host, key.Port, sslHostName: null, key.ProxyUri, key.Identity)
 0367            }
 368
 369            HttpConnectionPool? pool;
 0370            while (!_pools.TryGetValue(key, out pool))
 0371            {
 0372                pool = new HttpConnectionPool(this, key.Kind, key.Host, key.Port, sslHostName, key.ProxyUri, GetTelemetr
 373
 0374                if (_cleaningTimer == null)
 0375                {
 376                    // There's no cleaning timer, which means we're not adding connections into pools, but we still need
 377                    // the pool object for this request.  We don't need or want to add the pool to the pools, though,
 378                    // since we don't want it to sit there forever, which it would without the cleaning timer.
 0379                    break;
 380                }
 381
 0382                if (_pools.TryAdd(key, pool))
 0383                {
 384                    // We need to ensure the cleanup timer is running if it isn't
 385                    // already now that we added a new connection pool.
 0386                    lock (SyncObj)
 0387                    {
 0388                        if (!_timerIsRunning)
 0389                        {
 0390                            SetCleaningTimer(_cleanPoolTimeout);
 0391                        }
 0392                    }
 0393                    break;
 394                }
 395
 396                // We created a pool and tried to add it to our pools, but some other thread got there before us.
 397                // We don't need to Dispose the pool, as that's only needed when it contains connections
 398                // that need to be closed.
 0399            }
 400
 0401            return pool.SendAsync(request, async, doRequestAuth, cancellationToken);
 0402        }
 403
 404        public ValueTask<HttpResponseMessage> SendProxyConnectAsync(HttpRequestMessage request, Uri proxyUri, bool async
 0405        {
 0406            return SendAsyncCore(request, proxyUri, async, doRequestAuth: false, isProxyConnect: true, cancellationToken
 0407        }
 408
 409        public ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, Canc
 0410        {
 0411            if (_proxy == null)
 0412            {
 0413                return SendAsyncCore(request, null, async, doRequestAuth, isProxyConnect: false, cancellationToken);
 414            }
 415
 416            // Do proxy lookup.
 0417            Uri? proxyUri = null;
 418            try
 0419            {
 0420                Debug.Assert(request.RequestUri != null);
 0421                if (!_proxy.IsBypassed(request.RequestUri))
 0422                {
 0423                    if (_proxy is IMultiWebProxy multiWebProxy)
 0424                    {
 0425                        MultiProxy multiProxy = multiWebProxy.GetMultiProxy(request.RequestUri);
 426
 0427                        if (multiProxy.ReadNext(out proxyUri, out bool isFinalProxy) && !isFinalProxy)
 0428                        {
 0429                            return SendAsyncMultiProxy(request, async, doRequestAuth, multiProxy, proxyUri, cancellation
 430                        }
 0431                    }
 432                    else
 0433                    {
 0434                        proxyUri = _proxy.GetProxy(request.RequestUri);
 0435                    }
 0436                }
 0437            }
 0438            catch (Exception ex)
 0439            {
 440                // Eat any exception from the IWebProxy and just treat it as no proxy.
 441                // This matches the behavior of other handlers.
 0442                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Exception from {_proxy.GetType().Name}.
 0443            }
 444
 0445            if (proxyUri != null && !HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme))
 0446            {
 0447                throw new NotSupportedException(SR.net_http_invalid_proxy_scheme);
 448            }
 449
 0450            return SendAsyncCore(request, proxyUri, async, doRequestAuth, isProxyConnect: false, cancellationToken);
 0451        }
 452
 453        /// <summary>
 454        /// Iterates a request over a set of proxies until one works, or all proxies have failed.
 455        /// </summary>
 456        /// <param name="request">The request message.</param>
 457        /// <param name="async">Whether to execute the request synchronously or asynchronously.</param>
 458        /// <param name="doRequestAuth">Whether to perform request authentication.</param>
 459        /// <param name="multiProxy">The set of proxies to use.</param>
 460        /// <param name="firstProxy">The first proxy try.</param>
 461        /// <param name="cancellationToken">The cancellation token to use for the operation.</param>
 462        private async ValueTask<HttpResponseMessage> SendAsyncMultiProxy(HttpRequestMessage request, bool async, bool do
 0463        {
 464            HttpRequestException rethrowException;
 465
 466            // Save the original ProxyAuthorization header value so we can restore it when retrying with a different pro
 467            // This ensures that any proxy credentials set from the credential cache during a failed attempt are cleared
 468            // before trying the next proxy, while preserving any user-set credentials.
 0469            Headers.AuthenticationHeaderValue? originalProxyAuthorization = request.Headers.ProxyAuthorization;
 470
 471            do
 0472            {
 473                try
 0474                {
 0475                    return await SendAsyncCore(request, firstProxy, async, doRequestAuth, isProxyConnect: false, cancell
 476                }
 0477                catch (HttpRequestException ex) when (ex.AllowRetry != RequestRetryType.NoRetry)
 0478                {
 0479                    rethrowException = ex;
 480
 481                    // Clear any proxy-auth credentials that were set from the proxy credential cache for the previous p
 482                    // Restore the original value before retrying with the next proxy.
 0483                    request.Headers.ProxyAuthorization = originalProxyAuthorization;
 0484                }
 0485            }
 0486            while (multiProxy.ReadNext(out firstProxy, out _));
 487
 0488            ExceptionDispatchInfo.Throw(rethrowException);
 489            return null; // should never be reached: VS doesn't realize Throw() never returns.
 0490        }
 491
 492        /// <summary>Disposes of the pools, disposing of each individual pool.</summary>
 493        public void Dispose()
 0494        {
 0495            _disposed = true;
 0496            _cleaningTimer?.Dispose();
 0497            foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
 0498            {
 0499                pool.Value.Dispose();
 0500            }
 501
 0502            _networkChangeCleanup?.Dispose();
 0503        }
 504
 505        /// <summary>Sets <see cref="_cleaningTimer"/> and <see cref="_timerIsRunning"/> based on the specified timeout.
 506        private void SetCleaningTimer(TimeSpan timeout)
 0507        {
 0508            if (_cleaningTimer!.Change(timeout, Timeout.InfiniteTimeSpan))
 0509            {
 0510                _timerIsRunning = timeout != Timeout.InfiniteTimeSpan;
 0511            }
 0512        }
 513
 514        /// <summary>Removes unusable connections from each pool, and removes stale pools entirely.</summary>
 515        private void RemoveStalePools()
 0516        {
 0517            Debug.Assert(_cleaningTimer != null);
 518
 519            // Iterate through each pool in the set of pools.  For each, ask it to clear out
 520            // any unusable connections (e.g. those which have expired, those which have been closed, etc.)
 521            // The pool may detect that it's empty and long unused, in which case it'll dispose of itself,
 522            // such that any connections returned to the pool to be cached will be disposed of.  In such
 523            // a case, we also remove the pool from the set of pools to avoid a leak.
 0524            foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> entry in _pools)
 0525            {
 0526                if (entry.Value.CleanCacheAndDisposeIfUnused())
 0527                {
 0528                    _pools.TryRemove(entry.Key, out _);
 0529                }
 0530            }
 531
 532            // Restart the timer if we have any pools to clean up.
 0533            lock (SyncObj)
 0534            {
 0535                SetCleaningTimer(!_pools.IsEmpty ? _cleanPoolTimeout : Timeout.InfiniteTimeSpan);
 0536            }
 537
 538            // NOTE: There is a possible race condition with regards to a pool getting cleaned up at the same
 539            // time it's about to be used for another request.  The timer cleanup could start running, see that
 540            // a pool is empty, and initiate its disposal.  Concurrently, the pools could hand out the pool
 541            // to a request looking to get a connection, because the pool may not have been removed yet
 542            // from the pools.  Worst case here is that connection will end up getting returned to an
 543            // already disposed pool, in which case the connection will also end up getting disposed rather
 544            // than reused.  This should be a rare occurrence, so for now we don't worry about it.  In the
 545            // future, there are a variety of possible ways to address it, such as allowing connections to
 546            // be returned to pools they weren't associated with.
 0547        }
 548
 549        /// <summary>Sends keep alive PINGs on all pooled connections, and reports whether any connections remain.</summ
 550        private bool HeartBeat()
 0551        {
 0552            bool anyLiveConnections = false;
 553
 0554            foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
 0555            {
 0556                anyLiveConnections |= pool.Value.HeartBeat();
 0557            }
 558
 0559            return anyLiveConnections;
 0560        }
 561
 562        private static string GetIdentityIfDefaultCredentialsUsed(bool defaultCredentialsUsed)
 0563        {
 0564            return defaultCredentialsUsed ? CurrentUserIdentityProvider.GetIdentity() : string.Empty;
 0565        }
 566
 567        internal readonly struct HttpConnectionKey : IEquatable<HttpConnectionKey>
 568        {
 569            public readonly HttpConnectionKind Kind;
 570            public readonly string? Host;
 571            public readonly int Port;
 572            public readonly string? SslHostName;     // null if not SSL
 573            public readonly Uri? ProxyUri;
 574            public readonly string Identity;
 575
 576            public HttpConnectionKey(HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri
 0577            {
 0578                Kind = kind;
 0579                Host = host;
 0580                Port = port;
 0581                SslHostName = sslHostName;
 0582                ProxyUri = proxyUri;
 0583                Identity = identity;
 0584            }
 585
 586            // In the common case, SslHostName (when present) is equal to Host.  If so, don't include in hash.
 587            public override int GetHashCode() =>
 0588                (SslHostName == Host ?
 0589                    HashCode.Combine(Kind, Host, Port, ProxyUri, Identity) :
 0590                    HashCode.Combine(Kind, Host, Port, SslHostName, ProxyUri, Identity));
 591
 592            public override bool Equals([NotNullWhen(true)] object? obj) =>
 0593                obj is HttpConnectionKey hck &&
 0594                Equals(hck);
 595
 596            public bool Equals(HttpConnectionKey other) =>
 0597                Kind == other.Kind &&
 0598                Host == other.Host &&
 0599                Port == other.Port &&
 0600                ProxyUri == other.ProxyUri &&
 0601                SslHostName == other.SslHostName &&
 0602                Identity == other.Identity;
 603        }
 604    }
 605}
 606