CacheEntry.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Runtime.CompilerServices;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using Microsoft.Extensions.Primitives;
  9. using Microsoft.Extensions.Caching.Memory;
  10. namespace appMpower.Memory
  11. {
  12. internal sealed partial class CacheEntry : ICacheEntry
  13. {
  14. private static readonly Action<object> ExpirationCallback = ExpirationTokensExpired;
  15. private readonly MemoryCache _cache;
  16. private CacheEntryTokens _tokens; // might be null if user is not using the tokens or callbacks
  17. private TimeSpan? _absoluteExpirationRelativeToNow;
  18. private TimeSpan? _slidingExpiration;
  19. private long? _size;
  20. private CacheEntry _previous; // this field is not null only before the entry is added to the cache and tracking is enabled
  21. private object _value;
  22. private CacheEntryState _state;
  23. internal CacheEntry(object key, MemoryCache memoryCache)
  24. {
  25. Key = key ?? throw new ArgumentNullException(nameof(key));
  26. _cache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
  27. _previous = memoryCache.TrackLinkedCacheEntries ? CacheEntryHelper.EnterScope(this) : null;
  28. _state = new CacheEntryState(CacheItemPriority.Normal);
  29. }
  30. /// <summary>
  31. /// Gets or sets an absolute expiration date for the cache entry.
  32. /// </summary>
  33. public DateTimeOffset? AbsoluteExpiration { get; set; }
  34. /// <summary>
  35. /// Gets or sets an absolute expiration time, relative to now.
  36. /// </summary>
  37. public TimeSpan? AbsoluteExpirationRelativeToNow
  38. {
  39. get => _absoluteExpirationRelativeToNow;
  40. set
  41. {
  42. // this method does not set AbsoluteExpiration as it would require calling Clock.UtcNow twice:
  43. // once here and once in MemoryCache.SetEntry
  44. if (value <= TimeSpan.Zero)
  45. {
  46. throw new ArgumentOutOfRangeException(
  47. nameof(AbsoluteExpirationRelativeToNow),
  48. value,
  49. "The relative expiration value must be positive.");
  50. }
  51. _absoluteExpirationRelativeToNow = value;
  52. }
  53. }
  54. /// <summary>
  55. /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed.
  56. /// This will not extend the entry lifetime beyond the absolute expiration (if set).
  57. /// </summary>
  58. public TimeSpan? SlidingExpiration
  59. {
  60. get => _slidingExpiration;
  61. set
  62. {
  63. if (value <= TimeSpan.Zero)
  64. {
  65. throw new ArgumentOutOfRangeException(
  66. nameof(SlidingExpiration),
  67. value,
  68. "The sliding expiration value must be positive.");
  69. }
  70. _slidingExpiration = value;
  71. }
  72. }
  73. /// <summary>
  74. /// Gets the <see cref="IChangeToken"/> instances which cause the cache entry to expire.
  75. /// </summary>
  76. public IList<IChangeToken> ExpirationTokens => GetOrCreateTokens().ExpirationTokens;
  77. /// <summary>
  78. /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache.
  79. /// </summary>
  80. public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks => GetOrCreateTokens().PostEvictionCallbacks;
  81. /// <summary>
  82. /// Gets or sets the priority for keeping the cache entry in the cache during a
  83. /// memory pressure triggered cleanup. The default is <see cref="CacheItemPriority.Normal"/>.
  84. /// </summary>
  85. public CacheItemPriority Priority { get => _state.Priority; set => _state.Priority = value; }
  86. /// <summary>
  87. /// Gets or sets the size of the cache entry value.
  88. /// </summary>
  89. public long? Size
  90. {
  91. get => _size;
  92. set
  93. {
  94. if (value < 0)
  95. {
  96. throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(value)} must be non-negative.");
  97. }
  98. _size = value;
  99. }
  100. }
  101. public object Key { get; private set; }
  102. public object Value
  103. {
  104. get => _value;
  105. set
  106. {
  107. _value = value;
  108. _state.IsValueSet = true;
  109. }
  110. }
  111. internal DateTimeOffset LastAccessed { get; set; }
  112. internal EvictionReason EvictionReason { get => _state.EvictionReason; private set => _state.EvictionReason = value; }
  113. public void Dispose()
  114. {
  115. if (!_state.IsDisposed)
  116. {
  117. _state.IsDisposed = true;
  118. if (_cache.TrackLinkedCacheEntries)
  119. {
  120. CacheEntryHelper.ExitScope(this, _previous);
  121. }
  122. // Don't commit or propagate options if the CacheEntry Value was never set.
  123. // We assume an exception occurred causing the caller to not set the Value successfully,
  124. // so don't use this entry.
  125. if (_state.IsValueSet)
  126. {
  127. _cache.SetEntry(this);
  128. if (_previous != null && CanPropagateOptions())
  129. {
  130. PropagateOptions(_previous);
  131. }
  132. }
  133. _previous = null; // we don't want to root unnecessary objects
  134. }
  135. }
  136. [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
  137. internal bool CheckExpired(in DateTimeOffset now)
  138. => _state.IsExpired
  139. || CheckForExpiredTime(now)
  140. || (_tokens != null && _tokens.CheckForExpiredTokens(this));
  141. internal void SetExpired(EvictionReason reason)
  142. {
  143. if (EvictionReason == EvictionReason.None)
  144. {
  145. EvictionReason = reason;
  146. }
  147. _state.IsExpired = true;
  148. _tokens?.DetachTokens();
  149. }
  150. [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
  151. private bool CheckForExpiredTime(in DateTimeOffset now)
  152. {
  153. if (!AbsoluteExpiration.HasValue && !_slidingExpiration.HasValue)
  154. {
  155. return false;
  156. }
  157. return FullCheck(now);
  158. bool FullCheck(in DateTimeOffset offset)
  159. {
  160. if (AbsoluteExpiration.HasValue && AbsoluteExpiration.Value <= offset)
  161. {
  162. SetExpired(EvictionReason.Expired);
  163. return true;
  164. }
  165. if (_slidingExpiration.HasValue
  166. && (offset - LastAccessed) >= _slidingExpiration)
  167. {
  168. SetExpired(EvictionReason.Expired);
  169. return true;
  170. }
  171. return false;
  172. }
  173. }
  174. internal void AttachTokens() => _tokens?.AttachTokens(this);
  175. private static void ExpirationTokensExpired(object obj)
  176. {
  177. // start a new thread to avoid issues with callbacks called from RegisterChangeCallback
  178. Task.Factory.StartNew(state =>
  179. {
  180. var entry = (CacheEntry)state;
  181. entry.SetExpired(EvictionReason.TokenExpired);
  182. entry._cache.EntryExpired(entry);
  183. }, obj, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
  184. }
  185. internal void InvokeEvictionCallbacks() => _tokens?.InvokeEvictionCallbacks(this);
  186. // this simple check very often allows us to avoid expensive call to PropagateOptions(CacheEntryHelper.Current)
  187. [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
  188. internal bool CanPropagateOptions() => (_tokens != null && _tokens.CanPropagateTokens()) || AbsoluteExpiration.HasValue;
  189. internal void PropagateOptions(CacheEntry parent)
  190. {
  191. if (parent == null)
  192. {
  193. return;
  194. }
  195. // Copy expiration tokens and AbsoluteExpiration to the cache entries hierarchy.
  196. // We do this regardless of it gets cached because the tokens are associated with the value we'll return.
  197. _tokens?.PropagateTokens(parent);
  198. if (AbsoluteExpiration.HasValue)
  199. {
  200. if (!parent.AbsoluteExpiration.HasValue || AbsoluteExpiration < parent.AbsoluteExpiration)
  201. {
  202. parent.AbsoluteExpiration = AbsoluteExpiration;
  203. }
  204. }
  205. }
  206. private CacheEntryTokens GetOrCreateTokens()
  207. {
  208. if (_tokens != null)
  209. {
  210. return _tokens;
  211. }
  212. CacheEntryTokens result = new CacheEntryTokens();
  213. return Interlocked.CompareExchange(ref _tokens, result, null) ?? result;
  214. }
  215. }
  216. }