|
@@ -0,0 +1,520 @@
|
|
|
|
+// 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.Collections.Generic;
|
|
|
|
+using System.Runtime.CompilerServices;
|
|
|
|
+using System.Threading;
|
|
|
|
+using System.Threading.Tasks;
|
|
|
|
+using Microsoft.Extensions.Internal;
|
|
|
|
+using Microsoft.Extensions.Logging;
|
|
|
|
+using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
+using Microsoft.Extensions.Options;
|
|
|
|
+using Microsoft.Extensions.Caching.Memory;
|
|
|
|
+
|
|
|
|
+namespace appMpower.Memory
|
|
|
|
+{
|
|
|
|
+ /// <summary>
|
|
|
|
+ /// An implementation of <see cref="IMemoryCache"/> using a dictionary to
|
|
|
|
+ /// store its entries.
|
|
|
|
+ /// </summary>
|
|
|
|
+ public class MemoryCache : IMemoryCache
|
|
|
|
+ {
|
|
|
|
+ internal readonly ILogger _logger;
|
|
|
|
+
|
|
|
|
+ private readonly MemoryCacheOptions _options;
|
|
|
|
+ private readonly ConcurrentDictionary<object, CacheEntry> _entries;
|
|
|
|
+
|
|
|
|
+ private long _cacheSize;
|
|
|
|
+ private bool _disposed;
|
|
|
|
+ private DateTimeOffset _lastExpirationScan;
|
|
|
|
+
|
|
|
|
+ /// <summary>
|
|
|
|
+ /// Creates a new <see cref="MemoryCache"/> instance.
|
|
|
|
+ /// </summary>
|
|
|
|
+ /// <param name="optionsAccessor">The options of the cache.</param>
|
|
|
|
+ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor)
|
|
|
|
+ : this(optionsAccessor, NullLoggerFactory.Instance) { }
|
|
|
|
+
|
|
|
|
+ /// <summary>
|
|
|
|
+ /// Creates a new <see cref="MemoryCache"/> instance.
|
|
|
|
+ /// </summary>
|
|
|
|
+ /// <param name="optionsAccessor">The options of the cache.</param>
|
|
|
|
+ /// <param name="loggerFactory">The factory used to create loggers.</param>
|
|
|
|
+ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory loggerFactory)
|
|
|
|
+ {
|
|
|
|
+ if (optionsAccessor == null)
|
|
|
|
+ {
|
|
|
|
+ throw new ArgumentNullException(nameof(optionsAccessor));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (loggerFactory == null)
|
|
|
|
+ {
|
|
|
|
+ throw new ArgumentNullException(nameof(loggerFactory));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _options = optionsAccessor.Value;
|
|
|
|
+ //_logger = loggerFactory.CreateLogger<MemoryCache>();
|
|
|
|
+ _logger = loggerFactory.CreateLogger("MemoryCache");
|
|
|
|
+
|
|
|
|
+ _entries = new ConcurrentDictionary<object, CacheEntry>();
|
|
|
|
+
|
|
|
|
+ if (_options.Clock == null)
|
|
|
|
+ {
|
|
|
|
+ _options.Clock = new SystemClock();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _lastExpirationScan = _options.Clock.UtcNow;
|
|
|
|
+ TrackLinkedCacheEntries = _options.TrackLinkedCacheEntries; // we store the setting now so it's consistent for entire MemoryCache lifetime
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// <summary>
|
|
|
|
+ /// Cleans up the background collection events.
|
|
|
|
+ /// </summary>
|
|
|
|
+ ~MemoryCache() => Dispose(false);
|
|
|
|
+
|
|
|
|
+ /// <summary>
|
|
|
|
+ /// Gets the count of the current entries for diagnostic purposes.
|
|
|
|
+ /// </summary>
|
|
|
|
+ public int Count => _entries.Count;
|
|
|
|
+
|
|
|
|
+ // internal for testing
|
|
|
|
+ internal long Size { get => Interlocked.Read(ref _cacheSize); }
|
|
|
|
+
|
|
|
|
+ internal bool TrackLinkedCacheEntries { get; }
|
|
|
|
+
|
|
|
|
+ private ICollection<KeyValuePair<object, CacheEntry>> EntriesCollection => _entries;
|
|
|
|
+
|
|
|
|
+ /// <inheritdoc />
|
|
|
|
+ public ICacheEntry CreateEntry(object key)
|
|
|
|
+ {
|
|
|
|
+ CheckDisposed();
|
|
|
|
+ ValidateCacheKey(key);
|
|
|
|
+
|
|
|
|
+ return new CacheEntry(key, this);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ internal void SetEntry(CacheEntry entry)
|
|
|
|
+ {
|
|
|
|
+ if (_disposed)
|
|
|
|
+ {
|
|
|
|
+ // No-op instead of throwing since this is called during CacheEntry.Dispose
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (_options.SizeLimit.HasValue && !entry.Size.HasValue)
|
|
|
|
+ {
|
|
|
|
+ //throw new InvalidOperationException(SR.Format(SR.CacheEntryHasEmptySize, nameof(entry.Size), nameof(_options.SizeLimit)));
|
|
|
|
+ throw new InvalidOperationException();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ DateTimeOffset utcNow = _options.Clock.UtcNow;
|
|
|
|
+
|
|
|
|
+ DateTimeOffset? absoluteExpiration = null;
|
|
|
|
+ if (entry.AbsoluteExpirationRelativeToNow.HasValue)
|
|
|
|
+ {
|
|
|
|
+ absoluteExpiration = utcNow + entry.AbsoluteExpirationRelativeToNow;
|
|
|
|
+ }
|
|
|
|
+ else if (entry.AbsoluteExpiration.HasValue)
|
|
|
|
+ {
|
|
|
|
+ absoluteExpiration = entry.AbsoluteExpiration;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Applying the option's absolute expiration only if it's not already smaller.
|
|
|
|
+ // This can be the case if a dependent cache entry has a smaller value, and
|
|
|
|
+ // it was set by cascading it to its parent.
|
|
|
|
+ if (absoluteExpiration.HasValue)
|
|
|
|
+ {
|
|
|
|
+ if (!entry.AbsoluteExpiration.HasValue || absoluteExpiration.Value < entry.AbsoluteExpiration.Value)
|
|
|
|
+ {
|
|
|
|
+ entry.AbsoluteExpiration = absoluteExpiration;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Initialize the last access timestamp at the time the entry is added
|
|
|
|
+ entry.LastAccessed = utcNow;
|
|
|
|
+
|
|
|
|
+ if (_entries.TryGetValue(entry.Key, out CacheEntry priorEntry))
|
|
|
|
+ {
|
|
|
|
+ priorEntry.SetExpired(EvictionReason.Replaced);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool exceedsCapacity = UpdateCacheSizeExceedsCapacity(entry);
|
|
|
|
+
|
|
|
|
+ if (!entry.CheckExpired(utcNow) && !exceedsCapacity)
|
|
|
|
+ {
|
|
|
|
+ bool entryAdded = false;
|
|
|
|
+
|
|
|
|
+ if (priorEntry == null)
|
|
|
|
+ {
|
|
|
|
+ // Try to add the new entry if no previous entries exist.
|
|
|
|
+ entryAdded = _entries.TryAdd(entry.Key, entry);
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ // Try to update with the new entry if a previous entries exist.
|
|
|
|
+ entryAdded = _entries.TryUpdate(entry.Key, entry, priorEntry);
|
|
|
|
+
|
|
|
|
+ if (entryAdded)
|
|
|
|
+ {
|
|
|
|
+ if (_options.SizeLimit.HasValue)
|
|
|
|
+ {
|
|
|
|
+ // The prior entry was removed, decrease the by the prior entry's size
|
|
|
|
+ Interlocked.Add(ref _cacheSize, -priorEntry.Size.Value);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ // The update will fail if the previous entry was removed after retrival.
|
|
|
|
+ // Adding the new entry will succeed only if no entry has been added since.
|
|
|
|
+ // This guarantees removing an old entry does not prevent adding a new entry.
|
|
|
|
+ entryAdded = _entries.TryAdd(entry.Key, entry);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (entryAdded)
|
|
|
|
+ {
|
|
|
|
+ entry.AttachTokens();
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ if (_options.SizeLimit.HasValue)
|
|
|
|
+ {
|
|
|
|
+ // Entry could not be added, reset cache size
|
|
|
|
+ Interlocked.Add(ref _cacheSize, -entry.Size.Value);
|
|
|
|
+ }
|
|
|
|
+ entry.SetExpired(EvictionReason.Replaced);
|
|
|
|
+ entry.InvokeEvictionCallbacks();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (priorEntry != null)
|
|
|
|
+ {
|
|
|
|
+ priorEntry.InvokeEvictionCallbacks();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ if (exceedsCapacity)
|
|
|
|
+ {
|
|
|
|
+ // The entry was not added due to overcapacity
|
|
|
|
+ entry.SetExpired(EvictionReason.Capacity);
|
|
|
|
+
|
|
|
|
+ TriggerOvercapacityCompaction();
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ if (_options.SizeLimit.HasValue)
|
|
|
|
+ {
|
|
|
|
+ // Entry could not be added due to being expired, reset cache size
|
|
|
|
+ Interlocked.Add(ref _cacheSize, -entry.Size.Value);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ entry.InvokeEvictionCallbacks();
|
|
|
|
+ if (priorEntry != null)
|
|
|
|
+ {
|
|
|
|
+ RemoveEntry(priorEntry);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ StartScanForExpiredItemsIfNeeded(utcNow);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// <inheritdoc />
|
|
|
|
+ public bool TryGetValue(object key, out object result)
|
|
|
|
+ {
|
|
|
|
+ ValidateCacheKey(key);
|
|
|
|
+ CheckDisposed();
|
|
|
|
+
|
|
|
|
+ DateTimeOffset utcNow = _options.Clock.UtcNow;
|
|
|
|
+
|
|
|
|
+ if (_entries.TryGetValue(key, out CacheEntry entry))
|
|
|
|
+ {
|
|
|
|
+ // Check if expired due to expiration tokens, timers, etc. and if so, remove it.
|
|
|
|
+ // Allow a stale Replaced value to be returned due to concurrent calls to SetExpired during SetEntry.
|
|
|
|
+ if (!entry.CheckExpired(utcNow) || entry.EvictionReason == EvictionReason.Replaced)
|
|
|
|
+ {
|
|
|
|
+ entry.LastAccessed = utcNow;
|
|
|
|
+ result = entry.Value;
|
|
|
|
+
|
|
|
|
+ if (TrackLinkedCacheEntries && entry.CanPropagateOptions())
|
|
|
|
+ {
|
|
|
|
+ // When this entry is retrieved in the scope of creating another entry,
|
|
|
|
+ // that entry needs a copy of these expiration tokens.
|
|
|
|
+ entry.PropagateOptions(CacheEntryHelper.Current);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ StartScanForExpiredItemsIfNeeded(utcNow);
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ // TODO: For efficiency queue this up for batch removal
|
|
|
|
+ RemoveEntry(entry);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ StartScanForExpiredItemsIfNeeded(utcNow);
|
|
|
|
+
|
|
|
|
+ result = null;
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// <inheritdoc />
|
|
|
|
+ public void Remove(object key)
|
|
|
|
+ {
|
|
|
|
+ ValidateCacheKey(key);
|
|
|
|
+
|
|
|
|
+ CheckDisposed();
|
|
|
|
+ if (_entries.TryRemove(key, out CacheEntry entry))
|
|
|
|
+ {
|
|
|
|
+ if (_options.SizeLimit.HasValue)
|
|
|
|
+ {
|
|
|
|
+ Interlocked.Add(ref _cacheSize, -entry.Size.Value);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ entry.SetExpired(EvictionReason.Removed);
|
|
|
|
+ entry.InvokeEvictionCallbacks();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ StartScanForExpiredItemsIfNeeded(_options.Clock.UtcNow);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void RemoveEntry(CacheEntry entry)
|
|
|
|
+ {
|
|
|
|
+ if (EntriesCollection.Remove(new KeyValuePair<object, CacheEntry>(entry.Key, entry)))
|
|
|
|
+ {
|
|
|
|
+ if (_options.SizeLimit.HasValue)
|
|
|
|
+ {
|
|
|
|
+ Interlocked.Add(ref _cacheSize, -entry.Size.Value);
|
|
|
|
+ }
|
|
|
|
+ entry.InvokeEvictionCallbacks();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ internal void EntryExpired(CacheEntry entry)
|
|
|
|
+ {
|
|
|
|
+ // TODO: For efficiency consider processing these expirations in batches.
|
|
|
|
+ RemoveEntry(entry);
|
|
|
|
+ StartScanForExpiredItemsIfNeeded(_options.Clock.UtcNow);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Called by multiple actions to see how long it's been since we last checked for expired items.
|
|
|
|
+ // If sufficient time has elapsed then a scan is initiated on a background task.
|
|
|
|
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
+ private void StartScanForExpiredItemsIfNeeded(DateTimeOffset utcNow)
|
|
|
|
+ {
|
|
|
|
+ if (_options.ExpirationScanFrequency < utcNow - _lastExpirationScan)
|
|
|
|
+ {
|
|
|
|
+ ScheduleTask(utcNow);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void ScheduleTask(DateTimeOffset utcNow)
|
|
|
|
+ {
|
|
|
|
+ _lastExpirationScan = utcNow;
|
|
|
|
+ Task.Factory.StartNew(state => ScanForExpiredItems((MemoryCache)state), this,
|
|
|
|
+ CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static void ScanForExpiredItems(MemoryCache cache)
|
|
|
|
+ {
|
|
|
|
+ DateTimeOffset now = cache._lastExpirationScan = cache._options.Clock.UtcNow;
|
|
|
|
+
|
|
|
|
+ foreach (KeyValuePair<object, CacheEntry> item in cache._entries)
|
|
|
|
+ {
|
|
|
|
+ CacheEntry entry = item.Value;
|
|
|
|
+
|
|
|
|
+ if (entry.CheckExpired(now))
|
|
|
|
+ {
|
|
|
|
+ cache.RemoveEntry(entry);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private bool UpdateCacheSizeExceedsCapacity(CacheEntry entry)
|
|
|
|
+ {
|
|
|
|
+ if (!_options.SizeLimit.HasValue)
|
|
|
|
+ {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ long newSize = 0L;
|
|
|
|
+ for (int i = 0; i < 100; i++)
|
|
|
|
+ {
|
|
|
|
+ long sizeRead = Interlocked.Read(ref _cacheSize);
|
|
|
|
+ newSize = sizeRead + entry.Size.Value;
|
|
|
|
+
|
|
|
|
+ if (newSize < 0 || newSize > _options.SizeLimit)
|
|
|
|
+ {
|
|
|
|
+ // Overflow occurred, return true without updating the cache size
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (sizeRead == Interlocked.CompareExchange(ref _cacheSize, newSize, sizeRead))
|
|
|
|
+ {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void TriggerOvercapacityCompaction()
|
|
|
|
+ {
|
|
|
|
+ _logger.LogDebug("Overcapacity compaction triggered");
|
|
|
|
+
|
|
|
|
+ // Spawn background thread for compaction
|
|
|
|
+ ThreadPool.QueueUserWorkItem(s => OvercapacityCompaction((MemoryCache)s), this);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static void OvercapacityCompaction(MemoryCache cache)
|
|
|
|
+ {
|
|
|
|
+ long currentSize = Interlocked.Read(ref cache._cacheSize);
|
|
|
|
+
|
|
|
|
+ cache._logger.LogDebug($"Overcapacity compaction executing. Current size {currentSize}");
|
|
|
|
+
|
|
|
|
+ double? lowWatermark = cache._options.SizeLimit * (1 - cache._options.CompactionPercentage);
|
|
|
|
+ if (currentSize > lowWatermark)
|
|
|
|
+ {
|
|
|
|
+ cache.Compact(currentSize - (long)lowWatermark, entry => entry.Size.Value);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ cache._logger.LogDebug($"Overcapacity compaction executed. New size {Interlocked.Read(ref cache._cacheSize)}");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// Remove at least the given percentage (0.10 for 10%) of the total entries (or estimated memory?), according to the following policy:
|
|
|
|
+ /// 1. Remove all expired items.
|
|
|
|
+ /// 2. Bucket by CacheItemPriority.
|
|
|
|
+ /// 3. Least recently used objects.
|
|
|
|
+ /// ?. Items with the soonest absolute expiration.
|
|
|
|
+ /// ?. Items with the soonest sliding expiration.
|
|
|
|
+ /// ?. Larger objects - estimated by object graph size, inaccurate.
|
|
|
|
+ public void Compact(double percentage)
|
|
|
|
+ {
|
|
|
|
+ int removalCountTarget = (int)(_entries.Count * percentage);
|
|
|
|
+ Compact(removalCountTarget, _ => 1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntrySize)
|
|
|
|
+ {
|
|
|
|
+ var entriesToRemove = new List<CacheEntry>();
|
|
|
|
+ var lowPriEntries = new List<CacheEntry>();
|
|
|
|
+ var normalPriEntries = new List<CacheEntry>();
|
|
|
|
+ var highPriEntries = new List<CacheEntry>();
|
|
|
|
+ long removedSize = 0;
|
|
|
|
+
|
|
|
|
+ // Sort items by expired & priority status
|
|
|
|
+ DateTimeOffset now = _options.Clock.UtcNow;
|
|
|
|
+ foreach (KeyValuePair<object, CacheEntry> item in _entries)
|
|
|
|
+ {
|
|
|
|
+ CacheEntry entry = item.Value;
|
|
|
|
+ if (entry.CheckExpired(now))
|
|
|
|
+ {
|
|
|
|
+ entriesToRemove.Add(entry);
|
|
|
|
+ removedSize += computeEntrySize(entry);
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ switch (entry.Priority)
|
|
|
|
+ {
|
|
|
|
+ case CacheItemPriority.Low:
|
|
|
|
+ lowPriEntries.Add(entry);
|
|
|
|
+ break;
|
|
|
|
+ case CacheItemPriority.Normal:
|
|
|
|
+ normalPriEntries.Add(entry);
|
|
|
|
+ break;
|
|
|
|
+ case CacheItemPriority.High:
|
|
|
|
+ highPriEntries.Add(entry);
|
|
|
|
+ break;
|
|
|
|
+ case CacheItemPriority.NeverRemove:
|
|
|
|
+ break;
|
|
|
|
+ default:
|
|
|
|
+ throw new NotSupportedException("Not implemented: " + entry.Priority);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ ExpirePriorityBucket(ref removedSize, removalSizeTarget, computeEntrySize, entriesToRemove, lowPriEntries);
|
|
|
|
+ ExpirePriorityBucket(ref removedSize, removalSizeTarget, computeEntrySize, entriesToRemove, normalPriEntries);
|
|
|
|
+ ExpirePriorityBucket(ref removedSize, removalSizeTarget, computeEntrySize, entriesToRemove, highPriEntries);
|
|
|
|
+
|
|
|
|
+ foreach (CacheEntry entry in entriesToRemove)
|
|
|
|
+ {
|
|
|
|
+ RemoveEntry(entry);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Policy:
|
|
|
|
+ // 1. Least recently used objects.
|
|
|
|
+ // ?. Items with the soonest absolute expiration.
|
|
|
|
+ // ?. Items with the soonest sliding expiration.
|
|
|
|
+ // ?. Larger objects - estimated by object graph size, inaccurate.
|
|
|
|
+ static void ExpirePriorityBucket(ref long removedSize, long removalSizeTarget, Func<CacheEntry, long> computeEntrySize, List<CacheEntry> entriesToRemove, List<CacheEntry> priorityEntries)
|
|
|
|
+ {
|
|
|
|
+ // Do we meet our quota by just removing expired entries?
|
|
|
|
+ if (removalSizeTarget <= removedSize)
|
|
|
|
+ {
|
|
|
|
+ // No-op, we've met quota
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Expire enough entries to reach our goal
|
|
|
|
+ // TODO: Refine policy
|
|
|
|
+
|
|
|
|
+ // LRU
|
|
|
|
+ priorityEntries.Sort((e1, e2) => e1.LastAccessed.CompareTo(e2.LastAccessed));
|
|
|
|
+ foreach (CacheEntry entry in priorityEntries)
|
|
|
|
+ {
|
|
|
|
+ entry.SetExpired(EvictionReason.Capacity);
|
|
|
|
+ entriesToRemove.Add(entry);
|
|
|
|
+ removedSize += computeEntrySize(entry);
|
|
|
|
+
|
|
|
|
+ if (removalSizeTarget <= removedSize)
|
|
|
|
+ {
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void Dispose()
|
|
|
|
+ {
|
|
|
|
+ Dispose(true);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected virtual void Dispose(bool disposing)
|
|
|
|
+ {
|
|
|
|
+ if (!_disposed)
|
|
|
|
+ {
|
|
|
|
+ if (disposing)
|
|
|
|
+ {
|
|
|
|
+ GC.SuppressFinalize(this);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _disposed = true;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void CheckDisposed()
|
|
|
|
+ {
|
|
|
|
+ if (_disposed)
|
|
|
|
+ {
|
|
|
|
+ Throw();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ static void Throw() => throw new ObjectDisposedException(typeof(MemoryCache).FullName);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static void ValidateCacheKey(object key)
|
|
|
|
+ {
|
|
|
|
+ if (key == null)
|
|
|
|
+ {
|
|
|
|
+ Throw();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ static void Throw() => throw new ArgumentNullException(nameof(key));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|