| File: CacheEntry.cs | Web Access |
| Project: src\runtime\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) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.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.TotalOffsetMinutes; } } } 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; } } }