File: Internal\DefaultHybridCache.CacheItem.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.Diagnostics;
using System.Threading;
using Microsoft.Extensions.Caching.Memory;
 
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
 
internal partial class DefaultHybridCache
{
    internal abstract class CacheItem
    {
        private int _refCount = 1; // the number of pending operations against this cache item
 
        public abstract bool DebugIsImmutable { get; }
 
        // Note: the ref count is the number of callers anticipating this value at any given time. Initially,
        // it is one for a simple "get the value" flow, but if another call joins with us, it'll be incremented.
        // If either cancels, it will get decremented, with the entire flow being cancelled if it ever becomes
        // zero.
        // This counter also drives cache lifetime, with the cache itself incrementing the count by one. In the
        // case of mutable data, cache eviction may reduce this to zero (in cooperation with any concurrent readers,
        // who incr/decr around their fetch), allowing safe buffer recycling.
 
        internal int RefCount => Volatile.Read(ref _refCount);
 
        internal static readonly PostEvictionDelegate SharedOnEviction = static (key, value, reason, state) =>
        {
            if (value is CacheItem item)
            {
                _ = item.Release();
            }
        };
 
        public virtual bool NeedsEvictionCallback => false; // do we need to call Release when evicted?
 
        public abstract bool TryReserveBuffer(out BufferChunk buffer);
 
        /// <summary>
        /// Signal that the consumer is done with this item (ref-count decr).
        /// </summary>
        /// <returns>True if this is the final release.</returns>
        public bool Release()
        {
            int newCount = Interlocked.Decrement(ref _refCount);
            Debug.Assert(newCount >= 0, "over-release detected");
            if (newCount == 0)
            {
                // perform per-item clean-up, i.e. buffer recycling (if defensive copies needed)
                OnFinalRelease();
                return true;
            }
 
            return false;
        }
 
        public bool TryReserve()
        {
            // This is basically interlocked increment, but with a check against:
            // a) incrementing upwards from zero
            // b) overflowing *back* to zero
            int oldValue = Volatile.Read(ref _refCount);
            do
            {
                if (oldValue is 0 or -1)
                {
                    return false; // already burned, or about to roll around back to zero
                }
 
                var updated = Interlocked.CompareExchange(ref _refCount, oldValue + 1, oldValue);
                if (updated == oldValue)
                {
                    return true; // we exchanged
                }
 
                oldValue = updated; // we failed, but we have an updated state
            }
            while (true);
        }
 
        protected virtual void OnFinalRelease() // any required release semantics
        {
        }
    }
 
    internal abstract class CacheItem<T> : CacheItem
    {
        public abstract bool TryGetSize(out long size);
 
        // attempt to get a value that was *not* previously reserved
        public abstract bool TryGetValue(out T value);
 
        // get a value that *was* reserved, countermanding our reservation in the process
        public T GetReservedValue()
        {
            if (!TryGetValue(out var value))
            {
                Throw();
            }
 
            _ = Release();
            return value;
 
            static void Throw() => throw new ObjectDisposedException("The cache item has been recycled before the value was obtained");
        }
 
        internal static CacheItem<T> Create() => ImmutableTypeCache<T>.IsImmutable ? new ImmutableCacheItem<T>() : new MutableCacheItem<T>();
    }
}