| File: System\Net\Http\SocketsHttpHandler\FailedProxyCache.cs | Web Access |
| Project: src\runtime\src\libraries\System.Net.Http\src\System.Net.Http.csproj (System.Net.Http) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; namespace System.Net.Http { /// <summary> /// Holds a cache of failing proxies and manages when they should be retried. /// </summary> internal sealed class FailedProxyCache { /// <summary> /// When returned by <see cref="GetProxyRenewTicks"/>, indicates a proxy is immediately usable. /// </summary> public const long Immediate = 0; // If a proxy fails, time out 30 minutes. WinHTTP and Firefox both use this. private const int FailureTimeoutInMilliseconds = 1000 * 60 * 30; // Scan through the failures and flush any that have expired every 5 minutes. private const int FlushFailuresTimerInMilliseconds = 1000 * 60 * 5; // _failedProxies will only be flushed (rare but somewhat expensive) if we have more than this number of proxies in our dictionary. See Cleanup() for details. private const int LargeProxyConfigBoundary = 8; // Value is the Environment.TickCount64 to remove the proxy from the failure list. private readonly ConcurrentDictionary<Uri, long> _failedProxies = new ConcurrentDictionary<Uri, long>(); // When Environment.TickCount64 >= _nextFlushTicks, cause a flush. private long _nextFlushTicks = Environment.TickCount64 + FlushFailuresTimerInMilliseconds; // This lock can be folded into _nextFlushTicks for space optimization, but // this class should only have a single instance so would rather have clarity. private SpinLock _flushLock = new SpinLock(enableThreadOwnerTracking: false); // mutable struct; do not make this readonly /// <summary> /// Checks when a proxy will become usable. /// </summary> /// <param name="uri">The <see cref="Uri"/> of the proxy to check.</param> /// <returns>If the proxy can be used, <see cref="Immediate"/>. Otherwise, the next <see cref="Environment.TickCount64"/> that it should be used.</returns> public long GetProxyRenewTicks(Uri uri) { Cleanup(); // If not failed, ready immediately. if (!_failedProxies.TryGetValue(uri, out long renewTicks)) { return Immediate; } // If we haven't reached out renew time, the proxy can't be used. if (Environment.TickCount64 < renewTicks) { return renewTicks; } // Renew time reached, we can remove the proxy from the cache. if (TryRenewProxy(uri, renewTicks)) { return Immediate; } // Another thread updated the cache before we could remove it. // We can't know if this is a removal or an update, so check again. return _failedProxies.TryGetValue(uri, out renewTicks) ? renewTicks : Immediate; } /// <summary> /// Sets a proxy as failed, to avoid trying it again for some time. /// </summary> /// <param name="uri">The URI of the proxy.</param> public void SetProxyFailed(Uri uri) { _failedProxies[uri] = Environment.TickCount64 + FailureTimeoutInMilliseconds; Cleanup(); } /// <summary> /// Renews a proxy prior to its period expiring. Used when all proxies are failed to renew the proxy closest to being renewed. /// </summary> /// <param name="uri">The <paramref name="uri"/> of the proxy to renew.</param> /// <param name="renewTicks">The current renewal time for the proxy. If the value has changed from this, the proxy will not be renewed.</param> public bool TryRenewProxy(Uri uri, long renewTicks) => _failedProxies.TryRemove(new KeyValuePair<Uri, long>(uri, renewTicks)); /// <summary> /// Cleans up any old proxies that should no longer be marked as failing. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Cleanup() { if (_failedProxies.Count > LargeProxyConfigBoundary && Environment.TickCount64 >= Interlocked.Read(ref _nextFlushTicks)) { CleanupHelper(); } } /// <summary> /// Cleans up any old proxies that should no longer be marked as failing. /// </summary> /// <remarks> /// I expect this to never be called by <see cref="Cleanup"/> in a production system. It is only needed in the case /// that a system has a very large number of proxies that the PAC script cycles through. It is moderately expensive, /// so it's only run periodically and is disabled until we exceed <see cref="LargeProxyConfigBoundary"/> failed proxies. /// </remarks> [MethodImpl(MethodImplOptions.NoInlining)] private void CleanupHelper() { bool lockTaken = false; try { _flushLock.TryEnter(ref lockTaken); if (!lockTaken) { return; } long curTicks = Environment.TickCount64; foreach (KeyValuePair<Uri, long> kvp in _failedProxies) { if (curTicks >= kvp.Value) { ((ICollection<KeyValuePair<Uri, long>>)_failedProxies).Remove(kvp); } } } finally { if (lockTaken) { Interlocked.Exchange(ref _nextFlushTicks, Environment.TickCount64 + FlushFailuresTimerInMilliseconds); _flushLock.Exit(false); } } } } }