// 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.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Caching.Memory; namespace appMpower.Memory { internal sealed partial class CacheEntry : ICacheEntry { private static readonly Action ExpirationCallback = ExpirationTokensExpired; 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; 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 CacheEntryState _state; internal CacheEntry(object key, MemoryCache memoryCache) { Key = key ?? throw new ArgumentNullException(nameof(key)); _cache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); _previous = memoryCache.TrackLinkedCacheEntries ? CacheEntryHelper.EnterScope(this) : null; _state = new CacheEntryState(CacheItemPriority.Normal); } /// /// Gets or sets an absolute expiration date for the cache entry. /// public DateTimeOffset? AbsoluteExpiration { get; set; } /// /// Gets or sets an absolute expiration time, relative to now. /// public TimeSpan? AbsoluteExpirationRelativeToNow { get => _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 <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException( nameof(AbsoluteExpirationRelativeToNow), value, "The relative expiration value must be positive."); } _absoluteExpirationRelativeToNow = value; } } /// /// 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). /// public TimeSpan? SlidingExpiration { get => _slidingExpiration; set { if (value <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException( nameof(SlidingExpiration), value, "The sliding expiration value must be positive."); } _slidingExpiration = value; } } /// /// Gets the instances which cause the cache entry to expire. /// public IList ExpirationTokens => GetOrCreateTokens().ExpirationTokens; /// /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache. /// public IList PostEvictionCallbacks => GetOrCreateTokens().PostEvictionCallbacks; /// /// Gets or sets the priority for keeping the cache entry in the cache during a /// memory pressure triggered cleanup. The default is . /// public CacheItemPriority Priority { get => _state.Priority; set => _state.Priority = value; } /// /// Gets or sets the size of the cache entry value. /// public long? Size { get => _size; set { if (value < 0) { throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(value)} must be non-negative."); } _size = value; } } public object Key { get; private set; } public object Value { get => _value; set { _value = value; _state.IsValueSet = true; } } internal DateTimeOffset LastAccessed { get; set; } internal EvictionReason EvictionReason { get => _state.EvictionReason; private set => _state.EvictionReason = value; } public void Dispose() { if (!_state.IsDisposed) { _state.IsDisposed = true; if (_cache.TrackLinkedCacheEntries) { CacheEntryHelper.ExitScope(this, _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 (_state.IsValueSet) { _cache.SetEntry(this); if (_previous != null && CanPropagateOptions()) { PropagateOptions(_previous); } } _previous = null; // we don't want to root unnecessary objects } } [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling internal bool CheckExpired(in DateTimeOffset now) => _state.IsExpired || CheckForExpiredTime(now) || (_tokens != null && _tokens.CheckForExpiredTokens(this)); internal void SetExpired(EvictionReason reason) { if (EvictionReason == EvictionReason.None) { EvictionReason = reason; } _state.IsExpired = true; _tokens?.DetachTokens(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling private bool CheckForExpiredTime(in DateTimeOffset now) { if (!AbsoluteExpiration.HasValue && !_slidingExpiration.HasValue) { return false; } return FullCheck(now); bool FullCheck(in DateTimeOffset offset) { if (AbsoluteExpiration.HasValue && AbsoluteExpiration.Value <= offset) { SetExpired(EvictionReason.Expired); return true; } if (_slidingExpiration.HasValue && (offset - 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); // this simple check very often allows us to avoid expensive call to PropagateOptions(CacheEntryHelper.Current) [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling internal bool CanPropagateOptions() => (_tokens != null && _tokens.CanPropagateTokens()) || AbsoluteExpiration.HasValue; internal void PropagateOptions(CacheEntry parent) { if (parent == null) { 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. _tokens?.PropagateTokens(parent); if (AbsoluteExpiration.HasValue) { if (!parent.AbsoluteExpiration.HasValue || AbsoluteExpiration < parent.AbsoluteExpiration) { parent.AbsoluteExpiration = AbsoluteExpiration; } } } private CacheEntryTokens GetOrCreateTokens() { if (_tokens != null) { return _tokens; } CacheEntryTokens result = new CacheEntryTokens(); return Interlocked.CompareExchange(ref _tokens, result, null) ?? result; } } }