File: CacheEntry.cs
Web Access
Project: src\src\libraries\Microsoft.Extensions.Caching.Memory\src\Microsoft.Extensions.Caching.Memory.csproj (Microsoft.Extensions.Caching.Memory)
// 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.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.Extensions.Caching.Memory
{
    internal sealed partial class CacheEntry : ICacheEntry
    {
        private static readonly Action<object> ExpirationCallback = ExpirationTokensExpired;
        private static readonly AsyncLocal<CacheEntry?> _current = new AsyncLocal<CacheEntry?>();
 
        private readonly MemoryCache _cache;
 
        private CacheEntryTokens? _tokens; // might be null if user is not using the tokens or callbacks
        private TimeSpan _absoluteExpirationRelativeToNow;
        private TimeSpan _slidingExpiration;
        private long _size = NotSet;
        private CacheEntry? _previous; // this field is not null only before the entry is added to the cache and tracking is enabled
        private object? _value;
        private long _absoluteExpirationTicks = NotSet;
        private short _absoluteExpirationOffsetMinutes;
        private bool _isDisposed;
        private bool _isExpired;
        private bool _isValueSet;
        private byte _evictionReason;
        private byte _priority = (byte)CacheItemPriority.Normal;
 
        private const int NotSet = -1;
 
        internal CacheEntry(object key, MemoryCache memoryCache)
        {
            ThrowHelper.ThrowIfNull(key);
            ThrowHelper.ThrowIfNull(memoryCache);
 
            Key = key;
            _cache = memoryCache;
            if (memoryCache.TrackLinkedCacheEntries)
            {
                AsyncLocal<CacheEntry?> holder = _current;
                _previous = holder.Value;
                holder.Value = this;
            }
        }
 
        // internal for testing
        internal static CacheEntry? Current => _current.Value;
 
        internal long AbsoluteExpirationTicks
        {
            get => _absoluteExpirationTicks;
            set
            {
                _absoluteExpirationTicks = value;
                _absoluteExpirationOffsetMinutes = 0;
            }
        }
 
        DateTimeOffset? ICacheEntry.AbsoluteExpiration
        {
            get
            {
                if (_absoluteExpirationTicks < 0)
                    return null;
 
                var offset = new TimeSpan(_absoluteExpirationOffsetMinutes * TimeSpan.TicksPerMinute);
                return new DateTimeOffset(_absoluteExpirationTicks + offset.Ticks, offset);
            }
            set
            {
                if (value is null)
                {
                    _absoluteExpirationTicks = NotSet;
                    _absoluteExpirationOffsetMinutes = default;
                }
                else
                {
                    DateTimeOffset expiration = value.GetValueOrDefault();
                    _absoluteExpirationTicks = expiration.UtcTicks;
                    _absoluteExpirationOffsetMinutes = (short)(expiration.Offset.Ticks / TimeSpan.TicksPerMinute);
                }
            }
        }
 
        internal TimeSpan AbsoluteExpirationRelativeToNow => _absoluteExpirationRelativeToNow;
 
        TimeSpan? ICacheEntry.AbsoluteExpirationRelativeToNow
        {
            get => _absoluteExpirationRelativeToNow.Ticks == 0 ? null : _absoluteExpirationRelativeToNow;
            set
            {
                // this method does not set AbsoluteExpiration as it would require calling Clock.UtcNow twice:
                // once here and once in MemoryCache.SetEntry
 
                if (value is { Ticks: <= 0 })
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(AbsoluteExpirationRelativeToNow),
                        value,
                        "The relative expiration value must be positive.");
                }
 
                _absoluteExpirationRelativeToNow = value.GetValueOrDefault();
            }
        }
 
        /// <summary>
        /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed.
        /// This will not extend the entry lifetime beyond the absolute expiration (if set).
        /// </summary>
        public TimeSpan? SlidingExpiration
        {
            get => _slidingExpiration.Ticks == 0 ? null : _slidingExpiration;
            set
            {
                if (value is { Ticks: <= 0 })
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(SlidingExpiration),
                        value,
                        "The sliding expiration value must be positive.");
                }
 
                _slidingExpiration = value.GetValueOrDefault();
            }
        }
 
        /// <summary>
        /// Gets the <see cref="IChangeToken"/> instances which cause the cache entry to expire.
        /// </summary>
        [MemberNotNull(nameof(_tokens))]
        public IList<IChangeToken> ExpirationTokens => GetOrCreateTokens().ExpirationTokens;
 
        /// <summary>
        /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache.
        /// </summary>
        [MemberNotNull(nameof(_tokens))]
        public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks => GetOrCreateTokens().PostEvictionCallbacks;
 
        /// <summary>
        /// Gets or sets the priority for keeping the cache entry in the cache during a
        /// memory pressure triggered cleanup. The default is <see cref="CacheItemPriority.Normal"/>.
        /// </summary>
        public CacheItemPriority Priority { get => (CacheItemPriority)_priority; set => _priority = (byte)value; }
 
        internal long Size => _size;
 
        long? ICacheEntry.Size
        {
            get => _size < 0 ? null : _size;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(value)} must be non-negative.");
                }
 
                _size = value ?? NotSet;
            }
        }
 
        public object Key { get; }
 
        public object? Value
        {
            get => _value;
            set
            {
                _value = value;
                _isValueSet = true;
            }
        }
 
        internal DateTime LastAccessed { get; set; }
 
        internal EvictionReason EvictionReason { get => (EvictionReason)_evictionReason; private set => _evictionReason = (byte)value; }
 
        public void Dispose()
        {
            if (!_isDisposed)
            {
                _isDisposed = true;
 
                if (_cache.TrackLinkedCacheEntries)
                {
                    CommitWithTracking();
                }
                else if (_isValueSet)
                {
                    _cache.SetEntry(this);
                }
            }
        }
 
        private void CommitWithTracking()
        {
            Debug.Assert(_current.Value == this, "Entries disposed in invalid order");
            _current.Value = _previous;
 
            // Don't commit or propagate options if the CacheEntry Value was never set.
            // We assume an exception occurred causing the caller to not set the Value successfully,
            // so don't use this entry.
            if (_isValueSet)
            {
                _cache.SetEntry(this);
 
                CacheEntry? parent = _previous;
                if (parent != null)
                {
                    if ((ulong)_absoluteExpirationTicks < (ulong)parent._absoluteExpirationTicks)
                    {
                        parent._absoluteExpirationTicks = _absoluteExpirationTicks;
                        parent._absoluteExpirationOffsetMinutes = _absoluteExpirationOffsetMinutes;
                    }
                    _tokens?.PropagateTokens(parent);
                }
            }
 
            _previous = null; // we don't want to root unnecessary objects
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
        internal bool CheckExpired(DateTime utcNow)
            => _isExpired
                || CheckForExpiredTime(utcNow)
                || (_tokens != null && _tokens.CheckForExpiredTokens(this));
 
        internal void SetExpired(EvictionReason reason)
        {
            if (EvictionReason == EvictionReason.None)
            {
                EvictionReason = reason;
            }
            _isExpired = true;
            _tokens?.DetachTokens();
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
        private bool CheckForExpiredTime(DateTime utcNow)
        {
            if (_absoluteExpirationTicks < 0 && _slidingExpiration.Ticks == 0)
            {
                return false;
            }
 
            return FullCheck(utcNow);
 
            bool FullCheck(DateTime utcNow)
            {
                if ((ulong)_absoluteExpirationTicks <= (ulong)utcNow.Ticks)
                {
                    SetExpired(EvictionReason.Expired);
                    return true;
                }
 
                if (_slidingExpiration.Ticks > 0
                    && (utcNow - LastAccessed) >= _slidingExpiration)
                {
                    SetExpired(EvictionReason.Expired);
                    return true;
                }
 
                return false;
            }
        }
 
        internal void AttachTokens() => _tokens?.AttachTokens(this);
 
        private static void ExpirationTokensExpired(object obj)
        {
            // start a new thread to avoid issues with callbacks called from RegisterChangeCallback
            Task.Factory.StartNew(state =>
            {
                var entry = (CacheEntry)state!;
                entry.SetExpired(EvictionReason.TokenExpired);
                entry._cache.EntryExpired(entry);
            }, obj, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
        }
 
        internal void InvokeEvictionCallbacks() => _tokens?.InvokeEvictionCallbacks(this);
 
        internal void PropagateOptionsToCurrent()
        {
            if ((_tokens == null || !_tokens.CanPropagateTokens()) && _absoluteExpirationTicks < 0 || _current.Value is not CacheEntry parent)
            {
                return;
            }
 
            // Copy expiration tokens and AbsoluteExpiration to the cache entries hierarchy.
            // We do this regardless of it gets cached because the tokens are associated with the value we'll return.
            if ((ulong)_absoluteExpirationTicks < (ulong)parent._absoluteExpirationTicks)
            {
                parent._absoluteExpirationTicks = _absoluteExpirationTicks;
                parent._absoluteExpirationOffsetMinutes = _absoluteExpirationOffsetMinutes;
            }
 
            _tokens?.PropagateTokens(parent);
        }
 
        [MemberNotNull(nameof(_tokens))]
        private CacheEntryTokens GetOrCreateTokens()
        {
            if (_tokens != null)
            {
                return _tokens;
            }
 
            CacheEntryTokens result = new CacheEntryTokens();
            return Interlocked.CompareExchange(ref _tokens, result, null) ?? result;
        }
    }
}