File: Internal\DefaultHybridCache.TagInvalidation.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.Threading;
using System.Threading.Tasks;
 
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
 
internal partial class DefaultHybridCache
{
    private static readonly Task<long> _zeroTimestamp = Task.FromResult<long>(0L);
 
    private readonly ConcurrentDictionary<string, Task<long>> _tagInvalidationTimes = [];
 
#if NET9_0_OR_GREATER
    private readonly ConcurrentDictionary<string, Task<long>>.AlternateLookup<ReadOnlySpan<char>> _tagInvalidationTimesBySpan;
    private readonly bool _tagInvalidationTimesUseAltLookup;
#endif
 
    private Task<long> _globalInvalidateTimestamp;
 
    public override ValueTask RemoveByTagAsync(string tag, CancellationToken token = default)
    {
        if (string.IsNullOrWhiteSpace(tag))
        {
            return default; // nothing sensible to do
        }
 
        var now = CurrentTimestamp();
        InvalidateTagLocalCore(tag, now, isNow: true); // isNow to be 100% explicit
        return InvalidateL2TagAsync(tag, now, token);
    }
 
    public bool IsValid(CacheItem cacheItem)
    {
        var timestamp = cacheItem.CreationTimestamp;
 
        if (IsWildcardExpired(timestamp))
        {
            return false;
        }
 
        var tags = cacheItem.Tags;
        switch (tags.Count)
        {
            case 0:
                return true;
 
            case 1:
                return !IsTagExpired(tags.GetSinglePrechecked(), timestamp, out _);
 
            default:
                bool allValid = true;
                foreach (var tag in tags.GetSpanPrechecked())
                {
                    if (IsTagExpired(tag, timestamp, out _))
                    {
                        allValid = false; // but check them all, to kick-off tag fetch
                    }
                }
 
                return allValid;
        }
    }
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Completion-checked")]
    public bool IsWildcardExpired(long timestamp)
    {
        if (_globalInvalidateTimestamp.IsCompleted)
        {
            if (timestamp <= _globalInvalidateTimestamp.Result)
            {
                return true;
            }
        }
 
        return false;
    }
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Completion-checked")]
    public bool IsTagExpired(ReadOnlySpan<char> tag, long timestamp, out bool isPending)
    {
        isPending = false;
#if NET9_0_OR_GREATER
        if (_tagInvalidationTimesUseAltLookup && _tagInvalidationTimesBySpan.TryGetValue(tag, out var pending))
        {
            if (pending.IsCompleted)
            {
                return timestamp <= pending.Result;
            }
            else
            {
                isPending = true;
                return true; // assume invalid until completed
            }
        }
        else if (!HasBackendCache)
        {
            // not invalidated, and no L2 to check
            return false;
        }
#endif
 
        // fallback to using a string
        return IsTagExpired(tag.ToString(), timestamp, out isPending);
    }
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Completion-checked")]
    public bool IsTagExpired(string tag, long timestamp, out bool isPending)
    {
        isPending = false;
        if (!_tagInvalidationTimes.TryGetValue(tag, out var pending))
        {
            // not in the tag invalidation cache; if we have L2, need to check there
            if (HasBackendCache)
            {
                pending = SafeReadTagInvalidationAsync(tag);
                _ = _tagInvalidationTimes.TryAdd(tag, pending);
            }
            else
            {
                // not invalidated, and no L2 to check
                return false;
            }
        }
 
        if (pending.IsCompleted)
        {
            return timestamp <= pending.Result;
        }
        else
        {
            isPending = true;
            return true; // assume invalid until completed
        }
    }
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Ack")]
    public ValueTask<bool> IsAnyTagExpiredAsync(TagSet tags, long timestamp)
    {
        return tags.Count switch
        {
            0 => new(false),
            1 => IsTagExpiredAsync(tags.GetSinglePrechecked(), timestamp),
            _ => SlowAsync(this, tags, timestamp),
        };
 
        static async ValueTask<bool> SlowAsync(DefaultHybridCache @this, TagSet tags, long timestamp)
        {
            int count = tags.Count;
            for (int i = 0; i < count; i++)
            {
                if (await @this.IsTagExpiredAsync(tags[i], timestamp).ConfigureAwait(false))
                {
                    return true;
                }
            }
 
            return false;
        }
    }
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Ack")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1849:Call async methods when in an async method", Justification = "Completion-checked")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "Manual async unwrap")]
    public ValueTask<bool> IsTagExpiredAsync(string tag, long timestamp)
    {
        if (!_tagInvalidationTimes.TryGetValue(tag, out var pending))
        {
            // not in the tag invalidation cache; if we have L2, need to check there
            if (HasBackendCache)
            {
                pending = SafeReadTagInvalidationAsync(tag);
                _ = _tagInvalidationTimes.TryAdd(tag, pending);
            }
            else
            {
                // not invalidated, and no L2 to check
                return new(false);
            }
        }
 
        if (pending.IsCompleted)
        {
            return new(timestamp <= pending.Result);
        }
        else
        {
            return AwaitedAsync(pending, timestamp);
        }
 
        static async ValueTask<bool> AwaitedAsync(Task<long> pending, long timestamp) => timestamp <= await pending.ConfigureAwait(false);
    }
 
    internal void DebugInvalidateTag(string tag, Task<long> pending)
    {
        if (tag == TagSet.WildcardTag)
        {
            _globalInvalidateTimestamp = pending;
        }
        else
        {
            _tagInvalidationTimes[tag] = pending;
        }
    }
 
    internal long CurrentTimestamp() => _clock.GetUtcNow().UtcTicks;
 
    internal void PrefetchTags(TagSet tags)
    {
        if (HasBackendCache && !tags.IsEmpty)
        {
            // only needed if L2 exists
            switch (tags.Count)
            {
                case 1:
                    PrefetchTagWithBackendCache(tags.GetSinglePrechecked());
                    break;
                default:
                    foreach (var tag in tags.GetSpanPrechecked())
                    {
                        PrefetchTagWithBackendCache(tag);
                    }
 
                    break;
            }
        }
    }
 
    private void PrefetchTagWithBackendCache(string tag)
    {
        if (!_tagInvalidationTimes.TryGetValue(tag, out var pending))
        {
            _ = _tagInvalidationTimes.TryAdd(tag, SafeReadTagInvalidationAsync(tag));
        }
    }
 
    private void InvalidateTagLocalCore(string tag, long timestamp, bool isNow)
    {
        var timestampTask = Task.FromResult<long>(timestamp);
        if (tag == TagSet.WildcardTag)
        {
            _globalInvalidateTimestamp = timestampTask;
            if (isNow && !HasBackendCache)
            {
                // no L2, so we don't need any prior invalidated tags any more; can clear
                _tagInvalidationTimes.Clear();
            }
        }
        else
        {
            _tagInvalidationTimes[tag] = timestampTask;
 
            if (HybridCacheEventSource.Log.IsEnabled())
            {
                HybridCacheEventSource.Log.TagInvalidated();
            }
        }
    }
}