< Summary

Information
Class: System.Net.Http.HttpConnectionPoolManager
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\HttpConnectionPoolManager.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 281
Coverable lines: 281
Total lines: 560
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 128
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%26260%
StartMonitoringNetworkChanges()0%10100%
.ctor(...)100%110%
Finalize()100%110%
Dispose()100%110%
GetConnectionKey(...)0%20200%
GetTelemetryServerAddress(...)0%12120%
SendAsyncCore(...)0%880%
SendProxyConnectAsync(...)100%110%
SendAsync(...)0%16160%
SendAsyncMultiProxy()0%220%
Dispose()0%880%
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)

D:\runner\runtime\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.</summary>
 41        private readonly Timer? _heartBeatTimer;
 42
 43        private readonly HttpConnectionSettings _settings;
 44        private readonly IWebProxy? _proxy;
 45        private readonly ICredentials? _proxyCredentials;
 46
 47#if !ILLUMOS && !SOLARIS && !HAIKU
 48        private NetworkChangeCleanup? _networkChangeCleanup;
 49#endif
 50
 51        /// <summary>
 52        /// Keeps track of whether or not the cleanup timer is running. It helps us avoid the expensive
 53        /// <see cref="ConcurrentDictionary{TKey,TValue}.IsEmpty"/> call.
 54        /// </summary>
 55        private bool _timerIsRunning;
 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
 097                using (ExecutionContext.SuppressFlow()) // Don't capture the current ExecutionContext and its AsyncLocal
 098                {
 99                    // Create the timer.  Ensure the Timer has a weak reference to this manager; otherwise, it
 100                    // can introduce a cycle that keeps the HttpConnectionPoolManager rooted by the Timer
 101                    // implementation until the handler is Disposed (or indefinitely if it's not).
 0102                    var thisRef = new WeakReference<HttpConnectionPoolManager>(this);
 103
 0104                    _cleaningTimer = new Timer(static s =>
 0105                    {
 0106                        var wr = (WeakReference<HttpConnectionPoolManager>)s!;
 0107                        if (wr.TryGetTarget(out HttpConnectionPoolManager? thisRef))
 0108                        {
 0109                            thisRef.RemoveStalePools();
 0110                        }
 0111                    }, thisRef, Timeout.Infinite, Timeout.Infinite);
 112
 113
 114                    // For now heart beat is used only for ping functionality.
 0115                    if (_settings._keepAlivePingDelay != Timeout.InfiniteTimeSpan)
 0116                    {
 0117                        long heartBeatInterval = (long)Math.Max(1000, Math.Min(_settings._keepAlivePingDelay.TotalMillis
 118
 0119                        _heartBeatTimer = new Timer(static state =>
 0120                        {
 0121                            var wr = (WeakReference<HttpConnectionPoolManager>)state!;
 0122                            if (wr.TryGetTarget(out HttpConnectionPoolManager? thisRef))
 0123                            {
 0124                                thisRef.HeartBeat();
 0125                            }
 0126                        }, thisRef, heartBeatInterval, heartBeatInterval);
 0127                    }
 0128                }
 0129            }
 130
 131            // Figure out proxy stuff.
 0132            if (settings._useProxy)
 0133            {
 0134                _proxy = settings._proxy ?? HttpClient.DefaultProxy;
 0135                if (_proxy != null)
 0136                {
 0137                    _proxyCredentials = _proxy.Credentials ?? settings._defaultProxyCredentials;
 0138                }
 0139            }
 0140        }
 141
 142#if !ILLUMOS && !SOLARIS && !HAIKU
 143        /// <summary>
 144        /// Starts monitoring for network changes. Upon a change, <see cref="HttpConnectionPool.OnNetworkChanged"/> will
 145        /// called for every <see cref="HttpConnectionPool"/> in the <see cref="HttpConnectionPoolManager"/>.
 146        /// </summary>
 147        public void StartMonitoringNetworkChanges()
 0148        {
 0149            if (_networkChangeCleanup != null)
 0150            {
 0151                return;
 152            }
 153
 154            // Monitor network changes to invalidate Alt-Svc headers.
 155            // A weak reference is used to avoid NetworkChange.NetworkAddressChanged keeping a non-disposed connection p
 156            NetworkAddressChangedEventHandler networkChangedDelegate;
 0157            { // scope to avoid closure if _networkChangeCleanup != null
 0158                var poolsRef = new WeakReference<ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>>(_pools);
 0159                networkChangedDelegate = delegate
 0160                {
 0161                    if (poolsRef.TryGetTarget(out ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>? pools))
 0162                    {
 0163                        foreach (HttpConnectionPool pool in pools.Values)
 0164                        {
 0165                            pool.OnNetworkChanged();
 0166                        }
 0167                    }
 0168                };
 0169            }
 170
 0171            var cleanup = new NetworkChangeCleanup(networkChangedDelegate);
 172
 0173            if (Interlocked.CompareExchange(ref _networkChangeCleanup, cleanup, null) != null)
 0174            {
 175                // We lost a race, another thread already started monitoring.
 0176                GC.SuppressFinalize(cleanup);
 0177                return;
 178            }
 179
 180            // RFC: https://tools.ietf.org/html/rfc7838#section-2.2
 181            //    When alternative services are used to send a client to the most
 182            //    optimal server, a change in network configuration can result in
 183            //    cached values becoming suboptimal.  Therefore, clients SHOULD remove
 184            //    from cache all alternative services that lack the "persist" flag with
 185            //    the value "1" when they detect such a change, when information about
 186            //    network state is available.
 187            try
 0188            {
 0189                using (ExecutionContext.SuppressFlow())
 0190                {
 0191                    NetworkChange.NetworkAddressChanged += networkChangedDelegate;
 0192                }
 0193            }
 0194            catch (NetworkInformationException e)
 0195            {
 0196                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Exception when subscribing to NetworkCh
 197
 198                // We can't monitor network changes, so technically "information
 199                // about network state is not available" and we can just keep
 200                // all Alt-Svc entries until their expiration time.
 201                //
 202                // keep the _networkChangeCleanup field assigned so we don't try again needlessly
 0203            }
 0204        }
 205
 206        private sealed class NetworkChangeCleanup : IDisposable
 207        {
 208            private readonly NetworkAddressChangedEventHandler _handler;
 209
 0210            public NetworkChangeCleanup(NetworkAddressChangedEventHandler handler)
 0211            {
 0212                _handler = handler;
 0213            }
 214
 215            // If user never disposes the HttpClient, use finalizer to remove from NetworkChange.NetworkAddressChanged.
 216            // _handler will be rooted in NetworkChange, so should be safe to use here.
 0217            ~NetworkChangeCleanup() => NetworkChange.NetworkAddressChanged -= _handler;
 218
 219            public void Dispose()
 0220            {
 0221                NetworkChange.NetworkAddressChanged -= _handler;
 0222                GC.SuppressFinalize(this);
 0223            }
 224        }
 225#endif
 226
 0227        public HttpConnectionSettings Settings => _settings;
 0228        public ICredentials? ProxyCredentials => _proxyCredentials;
 229
 230        private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? proxyUri, bool isProxyConnect)
 0231        {
 0232            Uri? uri = request.RequestUri;
 0233            Debug.Assert(uri != null);
 234
 0235            if (isProxyConnect)
 0236            {
 0237                Debug.Assert(uri == proxyUri);
 0238                return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, uri.IdnHost, uri.Port, null, proxyUri, Get
 239            }
 240
 0241            string? sslHostName = null;
 0242            if (HttpUtilities.IsSupportedSecureScheme(uri.Scheme))
 0243            {
 0244                string? hostHeader = request.Headers.Host;
 0245                if (hostHeader != null)
 0246                {
 0247                    sslHostName = HttpUtilities.ParseHostNameFromHeader(hostHeader);
 0248                }
 249                else
 0250                {
 251                    // No explicit Host header. Use host from uri.
 0252                    sslHostName = uri.IdnHost;
 0253                }
 0254            }
 255
 0256            string identity = GetIdentityIfDefaultCredentialsUsed(proxyUri != null ? _settings._defaultCredentialsUsedFo
 257
 0258            if (proxyUri != null)
 0259            {
 0260                Debug.Assert(HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme));
 0261                if (HttpUtilities.IsSocksScheme(proxyUri.Scheme))
 0262                {
 263                    // Socks proxy
 0264                    if (sslHostName != null)
 0265                    {
 0266                        return new HttpConnectionKey(HttpConnectionKind.SslSocksTunnel, uri.IdnHost, uri.Port, sslHostNa
 267                    }
 268                    else
 0269                    {
 0270                        return new HttpConnectionKey(HttpConnectionKind.SocksTunnel, uri.IdnHost, uri.Port, null, proxyU
 271                    }
 272                }
 0273                else if (sslHostName == null)
 0274                {
 0275                    if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme))
 0276                    {
 277                        // Non-secure websocket connection through proxy to the destination.
 0278                        return new HttpConnectionKey(HttpConnectionKind.ProxyTunnel, uri.IdnHost, uri.Port, null, proxyU
 279                    }
 280                    else
 0281                    {
 282                        // Standard HTTP proxy usage for non-secure requests
 283                        // The destination host and port are ignored here, since these connections
 284                        // will be shared across any requests that use the proxy.
 0285                        return new HttpConnectionKey(HttpConnectionKind.Proxy, null, 0, null, proxyUri, identity);
 286                    }
 287                }
 288                else
 0289                {
 290                    // Tunnel SSL connection through proxy to the destination.
 0291                    return new HttpConnectionKey(HttpConnectionKind.SslProxyTunnel, uri.IdnHost, uri.Port, sslHostName, 
 292                }
 293            }
 0294            else if (sslHostName != null)
 0295            {
 0296                return new HttpConnectionKey(HttpConnectionKind.Https, uri.IdnHost, uri.Port, sslHostName, null, identit
 297            }
 298            else
 0299            {
 0300                return new HttpConnectionKey(HttpConnectionKind.Http, uri.IdnHost, uri.Port, null, null, identity);
 301            }
 0302        }
 303
 304        // Picks the value of the 'server.address' tag following rules specified in
 305        // https://github.com/open-telemetry/semantic-conventions/blob/728e5d1/docs/http/http-spans.md#http-client-span
 306        // When there is no proxy, we need to prioritize the contents of the Host header.
 307        private static string? GetTelemetryServerAddress(HttpRequestMessage request, HttpConnectionKey key)
 0308        {
 0309            if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled || GlobalHttpSettings.DiagnosticsHandler.EnableActiv
 0310            {
 0311                Uri? uri = request.RequestUri;
 0312                Debug.Assert(uri is not null);
 313
 0314                if (key.SslHostName is not null)
 0315                {
 0316                    return key.SslHostName;
 317                }
 318
 0319                if (key.ProxyUri is not null && key.Kind == HttpConnectionKind.Proxy)
 0320                {
 321                    // In case there is no tunnel, return the proxy address since the connection is shared.
 0322                    return key.ProxyUri.IdnHost;
 323                }
 324
 0325                string? hostHeader = request.Headers.Host;
 0326                return hostHeader is null ? uri.IdnHost : HttpUtilities.ParseHostNameFromHeader(hostHeader);
 327            }
 328
 0329            return null;
 0330        }
 331
 332        public ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, Uri? proxyUri, bool async, bool 
 0333        {
 0334            HttpConnectionKey key = GetConnectionKey(request, proxyUri, isProxyConnect);
 335
 336            HttpConnectionPool? pool;
 0337            while (!_pools.TryGetValue(key, out pool))
 0338            {
 0339                pool = new HttpConnectionPool(this, key.Kind, key.Host, key.Port, key.SslHostName, key.ProxyUri, GetTele
 340
 0341                if (_cleaningTimer == null)
 0342                {
 343                    // There's no cleaning timer, which means we're not adding connections into pools, but we still need
 344                    // the pool object for this request.  We don't need or want to add the pool to the pools, though,
 345                    // since we don't want it to sit there forever, which it would without the cleaning timer.
 0346                    break;
 347                }
 348
 0349                if (_pools.TryAdd(key, pool))
 0350                {
 351                    // We need to ensure the cleanup timer is running if it isn't
 352                    // already now that we added a new connection pool.
 0353                    lock (SyncObj)
 0354                    {
 0355                        if (!_timerIsRunning)
 0356                        {
 0357                            SetCleaningTimer(_cleanPoolTimeout);
 0358                        }
 0359                    }
 0360                    break;
 361                }
 362
 363                // We created a pool and tried to add it to our pools, but some other thread got there before us.
 364                // We don't need to Dispose the pool, as that's only needed when it contains connections
 365                // that need to be closed.
 0366            }
 367
 0368            return pool.SendAsync(request, async, doRequestAuth, cancellationToken);
 0369        }
 370
 371        public ValueTask<HttpResponseMessage> SendProxyConnectAsync(HttpRequestMessage request, Uri proxyUri, bool async
 0372        {
 0373            return SendAsyncCore(request, proxyUri, async, doRequestAuth: false, isProxyConnect: true, cancellationToken
 0374        }
 375
 376        public ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, Canc
 0377        {
 0378            if (_proxy == null)
 0379            {
 0380                return SendAsyncCore(request, null, async, doRequestAuth, isProxyConnect: false, cancellationToken);
 381            }
 382
 383            // Do proxy lookup.
 0384            Uri? proxyUri = null;
 385            try
 0386            {
 0387                Debug.Assert(request.RequestUri != null);
 0388                if (!_proxy.IsBypassed(request.RequestUri))
 0389                {
 0390                    if (_proxy is IMultiWebProxy multiWebProxy)
 0391                    {
 0392                        MultiProxy multiProxy = multiWebProxy.GetMultiProxy(request.RequestUri);
 393
 0394                        if (multiProxy.ReadNext(out proxyUri, out bool isFinalProxy) && !isFinalProxy)
 0395                        {
 0396                            return SendAsyncMultiProxy(request, async, doRequestAuth, multiProxy, proxyUri, cancellation
 397                        }
 0398                    }
 399                    else
 0400                    {
 0401                        proxyUri = _proxy.GetProxy(request.RequestUri);
 0402                    }
 0403                }
 0404            }
 0405            catch (Exception ex)
 0406            {
 407                // Eat any exception from the IWebProxy and just treat it as no proxy.
 408                // This matches the behavior of other handlers.
 0409                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Exception from {_proxy.GetType().Name}.
 0410            }
 411
 0412            if (proxyUri != null && !HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme))
 0413            {
 0414                throw new NotSupportedException(SR.net_http_invalid_proxy_scheme);
 415            }
 416
 0417            return SendAsyncCore(request, proxyUri, async, doRequestAuth, isProxyConnect: false, cancellationToken);
 0418        }
 419
 420        /// <summary>
 421        /// Iterates a request over a set of proxies until one works, or all proxies have failed.
 422        /// </summary>
 423        /// <param name="request">The request message.</param>
 424        /// <param name="async">Whether to execute the request synchronously or asynchronously.</param>
 425        /// <param name="doRequestAuth">Whether to perform request authentication.</param>
 426        /// <param name="multiProxy">The set of proxies to use.</param>
 427        /// <param name="firstProxy">The first proxy try.</param>
 428        /// <param name="cancellationToken">The cancellation token to use for the operation.</param>
 429        private async ValueTask<HttpResponseMessage> SendAsyncMultiProxy(HttpRequestMessage request, bool async, bool do
 0430        {
 431            HttpRequestException rethrowException;
 432
 433            do
 0434            {
 435                try
 0436                {
 0437                    return await SendAsyncCore(request, firstProxy, async, doRequestAuth, isProxyConnect: false, cancell
 438                }
 0439                catch (HttpRequestException ex) when (ex.AllowRetry != RequestRetryType.NoRetry)
 0440                {
 0441                    rethrowException = ex;
 0442                }
 0443            }
 0444            while (multiProxy.ReadNext(out firstProxy, out _));
 445
 0446            ExceptionDispatchInfo.Throw(rethrowException);
 447            return null; // should never be reached: VS doesn't realize Throw() never returns.
 0448        }
 449
 450        /// <summary>Disposes of the pools, disposing of each individual pool.</summary>
 451        public void Dispose()
 0452        {
 0453            _cleaningTimer?.Dispose();
 0454            _heartBeatTimer?.Dispose();
 0455            foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
 0456            {
 0457                pool.Value.Dispose();
 0458            }
 459
 460#if !ILLUMOS && !SOLARIS && !HAIKU
 0461            _networkChangeCleanup?.Dispose();
 462#endif
 0463        }
 464
 465        /// <summary>Sets <see cref="_cleaningTimer"/> and <see cref="_timerIsRunning"/> based on the specified timeout.
 466        private void SetCleaningTimer(TimeSpan timeout)
 0467        {
 0468            if (_cleaningTimer!.Change(timeout, Timeout.InfiniteTimeSpan))
 0469            {
 0470                _timerIsRunning = timeout != Timeout.InfiniteTimeSpan;
 0471            }
 0472        }
 473
 474        /// <summary>Removes unusable connections from each pool, and removes stale pools entirely.</summary>
 475        private void RemoveStalePools()
 0476        {
 0477            Debug.Assert(_cleaningTimer != null);
 478
 479            // Iterate through each pool in the set of pools.  For each, ask it to clear out
 480            // any unusable connections (e.g. those which have expired, those which have been closed, etc.)
 481            // The pool may detect that it's empty and long unused, in which case it'll dispose of itself,
 482            // such that any connections returned to the pool to be cached will be disposed of.  In such
 483            // a case, we also remove the pool from the set of pools to avoid a leak.
 0484            foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> entry in _pools)
 0485            {
 0486                if (entry.Value.CleanCacheAndDisposeIfUnused())
 0487                {
 0488                    _pools.TryRemove(entry.Key, out _);
 0489                }
 0490            }
 491
 492            // Restart the timer if we have any pools to clean up.
 0493            lock (SyncObj)
 0494            {
 0495                SetCleaningTimer(!_pools.IsEmpty ? _cleanPoolTimeout : Timeout.InfiniteTimeSpan);
 0496            }
 497
 498            // NOTE: There is a possible race condition with regards to a pool getting cleaned up at the same
 499            // time it's about to be used for another request.  The timer cleanup could start running, see that
 500            // a pool is empty, and initiate its disposal.  Concurrently, the pools could hand out the pool
 501            // to a request looking to get a connection, because the pool may not have been removed yet
 502            // from the pools.  Worst case here is that connection will end up getting returned to an
 503            // already disposed pool, in which case the connection will also end up getting disposed rather
 504            // than reused.  This should be a rare occurrence, so for now we don't worry about it.  In the
 505            // future, there are a variety of possible ways to address it, such as allowing connections to
 506            // be returned to pools they weren't associated with.
 0507        }
 508
 509        private void HeartBeat()
 0510        {
 0511            foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
 0512            {
 0513                pool.Value.HeartBeat();
 0514            }
 0515        }
 516
 517        private static string GetIdentityIfDefaultCredentialsUsed(bool defaultCredentialsUsed)
 0518        {
 0519            return defaultCredentialsUsed ? CurrentUserIdentityProvider.GetIdentity() : string.Empty;
 0520        }
 521
 522        internal readonly struct HttpConnectionKey : IEquatable<HttpConnectionKey>
 523        {
 524            public readonly HttpConnectionKind Kind;
 525            public readonly string? Host;
 526            public readonly int Port;
 527            public readonly string? SslHostName;     // null if not SSL
 528            public readonly Uri? ProxyUri;
 529            public readonly string Identity;
 530
 531            public HttpConnectionKey(HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri
 0532            {
 0533                Kind = kind;
 0534                Host = host;
 0535                Port = port;
 0536                SslHostName = sslHostName;
 0537                ProxyUri = proxyUri;
 0538                Identity = identity;
 0539            }
 540
 541            // In the common case, SslHostName (when present) is equal to Host.  If so, don't include in hash.
 542            public override int GetHashCode() =>
 0543                (SslHostName == Host ?
 0544                    HashCode.Combine(Kind, Host, Port, ProxyUri, Identity) :
 0545                    HashCode.Combine(Kind, Host, Port, SslHostName, ProxyUri, Identity));
 546
 547            public override bool Equals([NotNullWhen(true)] object? obj) =>
 0548                obj is HttpConnectionKey hck &&
 0549                Equals(hck);
 550
 551            public bool Equals(HttpConnectionKey other) =>
 0552                Kind == other.Kind &&
 0553                Host == other.Host &&
 0554                Port == other.Port &&
 0555                ProxyUri == other.ProxyUri &&
 0556                SslHostName == other.SslHostName &&
 0557                Identity == other.Identity;
 558        }
 559    }
 560}