File: Internal\DefaultHybridCache.Stampede.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Caching.Hybrid\Microsoft.Extensions.Caching.Hybrid.csproj (Microsoft.Extensions.Caching.Hybrid)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
 
internal partial class DefaultHybridCache
{
    private readonly ConcurrentDictionary<StampedeKey, StampedeState> _currentOperations = new();
 
    // returns true for a new session (in which case: we need to start the work), false for a pre-existing session
    public bool GetOrCreateStampedeState<TState, T>(string key, HybridCacheEntryFlags flags, out StampedeState<TState, T> stampedeState, bool canBeCanceled)
    {
        var stampedeKey = new StampedeKey(key, flags);
 
        // Double-checked locking to try to avoid unnecessary sessions in race conditions,
        // while avoiding the lock completely whenever possible.
        if (TryJoinExistingSession(this, stampedeKey, out var existing))
        {
            stampedeState = existing;
            return false; // someone ELSE is running the work
        }
 
        // Most common scenario here, then, is that we're not fighting with anyone else
        // go ahead and create a placeholder state object and *try* to add it.
        stampedeState = new StampedeState<TState, T>(this, stampedeKey, canBeCanceled);
        if (_currentOperations.TryAdd(stampedeKey, stampedeState))
        {
            // successfully added; indeed, no-one else was fighting: we're done
            return true; // the CURRENT caller is responsible for making the work happen
        }
 
        // Hmmm, failed to add - there's concurrent activity on the same key; we're now
        // in very rare race condition territory; go ahead and take a lock while we
        // collect our thoughts.
 
        // see notes in SyncLock.cs
        lock (GetPartitionedSyncLock(in stampedeKey))
        {
            // check again while we hold the lock
            if (TryJoinExistingSession(this, stampedeKey, out existing))
            {
                // we found an existing state we can join; do that
                stampedeState.SetCanceled(); // to be thorough: mark our speculative one as doomed (no-one has seen it, though)
                stampedeState = existing; // and replace with the one we found
                return false; // someone ELSE is running the work
 
                // Note that in this case we allocated a StampedeState<TState, T> that got dropped on
                // the floor; in the grand scheme of things, that's OK; this is a rare outcome.
            }
 
            // Check whether the value was L1-cached by an outgoing operation (for *us* to check needs local-cache-read,
            // and for *them* to have updated needs local-cache-write, but since the shared us/them key includes flags,
            // we can skip this if *either* flag is set).
            if ((flags & HybridCacheEntryFlags.DisableLocalCache) == 0 && _localCache.TryGetValue(key, out var untyped)
                && untyped is CacheItem<T> typed && typed.TryReserve())
            {
                stampedeState.SetResultDirect(typed);
                return false; // the work has ALREADY been done
            }
 
            // Otherwise, either nothing existed - or the thing that already exists can't be joined
            // in that case, go ahead and use the state that we invented a moment ago (outside of the lock).
            _currentOperations[stampedeKey] = stampedeState;
            return true; // the CURRENT caller is responsible for making the work happen
        }
 
        static bool TryJoinExistingSession(DefaultHybridCache @this, in StampedeKey stampedeKey,
            [NotNullWhen(true)] out StampedeState<TState, T>? stampedeState)
        {
            if (@this._currentOperations.TryGetValue(stampedeKey, out var found))
            {
                if (found is not StampedeState<TState, T> tmp)
                {
                    ThrowWrongType(stampedeKey.Key, found.Type, typeof(T));
                }
 
                if (tmp.TryAddCaller())
                {
                    // we joined an existing session
                    stampedeState = tmp;
                    return true;
                }
            }
 
            stampedeState = null;
            return false;
        }
 
        [DoesNotReturn]
        static void ThrowWrongType(string key, Type existingType, Type newType)
        {
            Debug.Assert(existingType != newType, "should be different types");
            throw new InvalidOperationException(
                $"All calls to {nameof(HybridCache)} with the same key should use the same data type; the same key is being used for '{existingType.FullName}' and '{newType.FullName}' data")
            {
                Data = { { "CacheKey", key } }
            };
        }
    }
 
    internal int DebugGetCallerCount(string key, HybridCacheEntryFlags? flags = null)
    {
        var stampedeKey = new StampedeKey(key, flags ?? _defaultFlags);
        return _currentOperations.TryGetValue(stampedeKey, out var state) ? state.DebugCallerCount : 0;
    }
}