File: src\runtime\src\libraries\Common\src\System\Net\SafeHandleCache.cs
Web Access
Project: src\runtime\src\libraries\System.Net.Security\src\System.Net.Security.csproj (System.Net.Security)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;

namespace System.Net
{
    internal interface ISafeHandleCachable
    {
        // Attempts to resever the handle for use. If the handle is already
        // disposed (or scheduled to be disposed), this will return false.
        //
        // each successful call to TryAddRentCount() must be paired with a Dispose() call.
        bool TryAddRentCount();

        // Marks the handle as scheduled for disposal if it is not being used.
        // Returns false if the handle is currently being used.
        // once marked, no new renters are allowed.
        bool TryMarkForDispose();
    }

    /// <summary>
    /// Helper class for implementing a cache for types deriving from <see
    /// cref="SafeHandle"/>. The purpose of the cache is to allow reuse of
    /// resources which may enable additional features (such as TLS resumption).
    /// The cache handles insertion and eviction in a thread-safe manner and
    /// implements simple mechanism for preventing unbounded growth and memory
    /// leaks.
    /// </summary>
    internal class SafeHandleCache<TKey, THandle> where TKey : IEquatable<TKey> where THandle : SafeHandle, ISafeHandleCachable
    {
        private const int CheckExpiredModulo = 32;

        private readonly ConcurrentDictionary<TKey, THandle> _cache = new();

        /// <summary>
        /// Gets the handle from the cache if it exists, otherwise creates a new one using the
        /// provided factory function and context.
        ///
        /// In case of two racing inserts with the same key, the handle returned by the factory may
        /// end up being discarded in favor of the one that was inserted first.  In such case, the
        /// factory handle is disposed and the cached handle is returned.
        ///
        /// The handle returned from this function should be disposed exactly once when it is no
        /// longer needed.
        /// </summary>
        internal THandle GetOrCreate<TContext>(TKey key, Func<TContext, THandle> factory, TContext factoryContext)
        {
            if (_cache.TryGetValue(key, out THandle? handle) && handle.TryAddRentCount())
            {
                if (NetEventSource.Log.IsEnabled())
                {
                    NetEventSource.Info(this, $"Found cached {handle}.");
                }
                return handle;
            }

            // if we get here, the handle is either not in the cache, or we lost
            // the race between TryAddRentCount on this thread and
            // MarkForDispose on another thread doing cache cleanup.  In either
            // case, we need to create a new handle.

            handle = factory(factoryContext);
            handle.TryAddRentCount(); // The caler is the first renter

            THandle cached;
            do
            {
                cached = _cache.GetOrAdd(key, handle);
            }
            // If we get the same handle back, we successfully added it to the cache and we are done.
            // If we get a different handle back, we need to increase the rent count.
            // If we fail to add the rent count, then the existing/cached handle is in process of
            // being removed from the cache and we can try again, eventually either succeeding to
            // add our new handle or getting a fresh handle inserted by another thread meanwhile.
            while (cached != handle && !cached.TryAddRentCount());

            if (cached != handle)
            {
                // we lost a race with another thread to insert new handle into the cache
                if (NetEventSource.Log.IsEnabled())
                {
                    NetEventSource.Info(this, $"Discarding {handle} (preferring cached {cached}).");
                }

                // First dispose decrements the rent count we added before attempting the cache insertion
                // and second closes the handle
                handle.Dispose();
                handle.Dispose();
                Debug.Assert(handle.IsClosed);

                return cached;
            }

            CheckForCleanup();

            return handle;
        }

        private void CheckForCleanup()
        {
            // We check the cache size after every couple of insertions, and
            // discard all handles which are not being actively rented. This
            // should still be flexible enough to allow "stable set" of
            // arbitrary size, while still preventing unbounded growth.

            var count = _cache.Count;
            if (count % CheckExpiredModulo == 0)
            {
                // let only one thread perform cleanup at a time
                lock (_cache)
                {
                    // check again, if another thread just cleaned up (and cached count went down) we are unlikely
                    // to clean anything
                    if (_cache.Count >= count)
                    {
                        if (NetEventSource.Log.IsEnabled())
                        {
                            NetEventSource.Info(this, $"Current size: {_cache.Count}.");
                        }

                        foreach ((TKey key, THandle handle) in _cache)
                        {
                            if (!handle.TryMarkForDispose())
                            {
                                // handle in use
                                continue;
                            }

                            // the handle is not in use and has been marked such that no new rents can be added.
                            if (NetEventSource.Log.IsEnabled())
                            {
                                NetEventSource.Info(this, $"Evicting cached {handle}.");
                            }

                            bool removed = _cache.TryRemove(key, out _);
                            Debug.Assert(removed);
                            handle.Dispose();

                            // Since the handle is not used anywhere, this should close the handle
                            Debug.Assert(handle.IsClosed);
                        }

                        if (NetEventSource.Log.IsEnabled())
                        {
                            NetEventSource.Info(this, $"New size: {_cache.Count}.");
                        }
                    }
                }
            }
        }
    }
}