Skip to content

Commit

Permalink
Add Clear() to MemoryCache (#57631)
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Erhardt <[email protected]>
  • Loading branch information
adamsitnik and eerhardt authored Nov 23, 2021
1 parent 8193214 commit 092c2ab
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ protected virtual void Dispose(bool disposing) { }
~MemoryCache() { }
public void Remove(object key) { }
public bool TryGetValue(object key, out object result) { throw null; }
public void Clear() { }
}
public partial class MemoryCacheOptions : Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions>
{
Expand Down
114 changes: 72 additions & 42 deletions src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ public class MemoryCache : IMemoryCache
internal readonly ILogger _logger;

private readonly MemoryCacheOptions _options;
private readonly ConcurrentDictionary<object, CacheEntry> _entries;

private long _cacheSize;
private CoherentState _coherentState;
private bool _disposed;
private DateTimeOffset _lastExpirationScan;

Expand Down Expand Up @@ -56,7 +55,7 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory
_options = optionsAccessor.Value;
_logger = loggerFactory.CreateLogger<MemoryCache>();

_entries = new ConcurrentDictionary<object, CacheEntry>();
_coherentState = new();

if (_options.Clock == null)
{
Expand All @@ -75,15 +74,13 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory
/// <summary>
/// Gets the count of the current entries for diagnostic purposes.
/// </summary>
public int Count => _entries.Count;
public int Count => _coherentState.Count;

// internal for testing
internal long Size { get => Interlocked.Read(ref _cacheSize); }
internal long Size => _coherentState.Size;

internal bool TrackLinkedCacheEntries { get; }

private ICollection<KeyValuePair<object, CacheEntry>> EntriesCollection => _entries;

/// <inheritdoc />
public ICacheEntry CreateEntry(object key)
{
Expand Down Expand Up @@ -123,7 +120,8 @@ internal void SetEntry(CacheEntry entry)
// Initialize the last access timestamp at the time the entry is added
entry.LastAccessed = utcNow;

if (_entries.TryGetValue(entry.Key, out CacheEntry priorEntry))
CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime
if (coherentState._entries.TryGetValue(entry.Key, out CacheEntry priorEntry))
{
priorEntry.SetExpired(EvictionReason.Replaced);
}
Expand All @@ -133,41 +131,41 @@ internal void SetEntry(CacheEntry entry)
entry.InvokeEvictionCallbacks();
if (priorEntry != null)
{
RemoveEntry(priorEntry);
coherentState.RemoveEntry(priorEntry, _options);
}
StartScanForExpiredItemsIfNeeded(utcNow);
return;
}

bool exceedsCapacity = UpdateCacheSizeExceedsCapacity(entry);
bool exceedsCapacity = UpdateCacheSizeExceedsCapacity(entry, coherentState);
if (!exceedsCapacity)
{
bool entryAdded = false;

if (priorEntry == null)
{
// Try to add the new entry if no previous entries exist.
entryAdded = _entries.TryAdd(entry.Key, entry);
entryAdded = coherentState._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);
entryAdded = coherentState._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);
Interlocked.Add(ref coherentState._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);
entryAdded = coherentState._entries.TryAdd(entry.Key, entry);
}
}

Expand All @@ -180,7 +178,7 @@ internal void SetEntry(CacheEntry entry)
if (_options.SizeLimit.HasValue)
{
// Entry could not be added, reset cache size
Interlocked.Add(ref _cacheSize, -entry.Size.Value);
Interlocked.Add(ref coherentState._cacheSize, -entry.Size.Value);
}
entry.SetExpired(EvictionReason.Replaced);
entry.InvokeEvictionCallbacks();
Expand All @@ -198,7 +196,7 @@ internal void SetEntry(CacheEntry entry)
entry.InvokeEvictionCallbacks();
if (priorEntry != null)
{
RemoveEntry(priorEntry);
coherentState.RemoveEntry(priorEntry, _options);
}
}

Expand All @@ -213,7 +211,8 @@ public bool TryGetValue(object key, out object result)

DateTimeOffset utcNow = _options.Clock.UtcNow;

if (_entries.TryGetValue(key, out CacheEntry entry))
CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime
if (coherentState._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.
Expand All @@ -236,7 +235,7 @@ public bool TryGetValue(object key, out object result)
else
{
// TODO: For efficiency queue this up for batch removal
RemoveEntry(entry);
coherentState.RemoveEntry(entry, _options);
}
}

Expand All @@ -250,13 +249,14 @@ public bool TryGetValue(object key, out object result)
public void Remove(object key)
{
ValidateCacheKey(key);

CheckDisposed();
if (_entries.TryRemove(key, out CacheEntry entry))

CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime
if (coherentState._entries.TryRemove(key, out CacheEntry entry))
{
if (_options.SizeLimit.HasValue)
{
Interlocked.Add(ref _cacheSize, -entry.Size.Value);
Interlocked.Add(ref coherentState._cacheSize, -entry.Size.Value);
}

entry.SetExpired(EvictionReason.Removed);
Expand All @@ -266,22 +266,25 @@ public void Remove(object key)
StartScanForExpiredItemsIfNeeded(_options.Clock.UtcNow);
}

private void RemoveEntry(CacheEntry entry)
/// <summary>
/// Removes all keys and values from the cache.
/// </summary>
public void Clear()
{
if (EntriesCollection.Remove(new KeyValuePair<object, CacheEntry>(entry.Key, entry)))
CheckDisposed();

CoherentState oldState = Interlocked.Exchange(ref _coherentState, new CoherentState());
foreach (var entry in oldState._entries)
{
if (_options.SizeLimit.HasValue)
{
Interlocked.Add(ref _cacheSize, -entry.Size.Value);
}
entry.InvokeEvictionCallbacks();
entry.Value.SetExpired(EvictionReason.Removed);
entry.Value.InvokeEvictionCallbacks();
}
}

internal void EntryExpired(CacheEntry entry)
{
// TODO: For efficiency consider processing these expirations in batches.
RemoveEntry(entry);
_coherentState.RemoveEntry(entry, _options);
StartScanForExpiredItemsIfNeeded(_options.Clock.UtcNow);
}

Expand All @@ -307,18 +310,19 @@ private static void ScanForExpiredItems(MemoryCache cache)
{
DateTimeOffset now = cache._lastExpirationScan = cache._options.Clock.UtcNow;

foreach (KeyValuePair<object, CacheEntry> item in cache._entries)
CoherentState coherentState = cache._coherentState; // Clear() can update the reference in the meantime
foreach (KeyValuePair<object, CacheEntry> item in coherentState._entries)
{
CacheEntry entry = item.Value;

if (entry.CheckExpired(now))
{
cache.RemoveEntry(entry);
coherentState.RemoveEntry(entry, cache._options);
}
}
}

private bool UpdateCacheSizeExceedsCapacity(CacheEntry entry)
private bool UpdateCacheSizeExceedsCapacity(CacheEntry entry, CoherentState coherentState)
{
if (!_options.SizeLimit.HasValue)
{
Expand All @@ -328,7 +332,7 @@ private bool UpdateCacheSizeExceedsCapacity(CacheEntry entry)
long newSize = 0L;
for (int i = 0; i < 100; i++)
{
long sizeRead = Interlocked.Read(ref _cacheSize);
long sizeRead = coherentState.Size;
newSize = sizeRead + entry.Size.Value;

if (newSize < 0 || newSize > _options.SizeLimit)
Expand All @@ -337,7 +341,7 @@ private bool UpdateCacheSizeExceedsCapacity(CacheEntry entry)
return true;
}

if (sizeRead == Interlocked.CompareExchange(ref _cacheSize, newSize, sizeRead))
if (sizeRead == Interlocked.CompareExchange(ref coherentState._cacheSize, newSize, sizeRead))
{
return false;
}
Expand All @@ -356,17 +360,18 @@ private void TriggerOvercapacityCompaction()

private static void OvercapacityCompaction(MemoryCache cache)
{
long currentSize = Interlocked.Read(ref cache._cacheSize);
CoherentState coherentState = cache._coherentState; // Clear() can update the reference in the meantime
long currentSize = coherentState.Size;

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.Compact(currentSize - (long)lowWatermark, entry => entry.Size.Value, coherentState);
}

cache._logger.LogDebug($"Overcapacity compaction executed. New size {Interlocked.Read(ref cache._cacheSize)}");
cache._logger.LogDebug($"Overcapacity compaction executed. New size {coherentState.Size}");
}

/// Remove at least the given percentage (0.10 for 10%) of the total entries (or estimated memory?), according to the following policy:
Expand All @@ -378,11 +383,12 @@ private static void OvercapacityCompaction(MemoryCache cache)
/// ?. Larger objects - estimated by object graph size, inaccurate.
public void Compact(double percentage)
{
int removalCountTarget = (int)(_entries.Count * percentage);
Compact(removalCountTarget, _ => 1);
CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime
int removalCountTarget = (int)(coherentState.Count * percentage);
Compact(removalCountTarget, _ => 1, coherentState);
}

private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntrySize)
private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntrySize, CoherentState coherentState)
{
var entriesToRemove = new List<CacheEntry>();
var lowPriEntries = new List<CacheEntry>();
Expand All @@ -392,7 +398,7 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry

// Sort items by expired & priority status
DateTimeOffset now = _options.Clock.UtcNow;
foreach (KeyValuePair<object, CacheEntry> item in _entries)
foreach (KeyValuePair<object, CacheEntry> item in coherentState._entries)
{
CacheEntry entry = item.Value;
if (entry.CheckExpired(now))
Expand Down Expand Up @@ -427,7 +433,7 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry

foreach (CacheEntry entry in entriesToRemove)
{
RemoveEntry(entry);
coherentState.RemoveEntry(entry, _options);
}

// Policy:
Expand Down Expand Up @@ -500,5 +506,29 @@ private static void ValidateCacheKey(object key)

static void Throw() => throw new ArgumentNullException(nameof(key));
}

private sealed class CoherentState
{
internal ConcurrentDictionary<object, CacheEntry> _entries = new ConcurrentDictionary<object, CacheEntry>();
internal long _cacheSize;

private ICollection<KeyValuePair<object, CacheEntry>> EntriesCollection => _entries;

internal int Count => _entries.Count;

internal long Size => Interlocked.Read(ref _cacheSize);

internal void RemoveEntry(CacheEntry entry, MemoryCacheOptions options)
{
if (EntriesCollection.Remove(new KeyValuePair<object, CacheEntry>(entry.Key, entry)))
{
if (options.SizeLimit.HasValue)
{
Interlocked.Add(ref _cacheSize, -entry.Size.Value);
}
entry.InvokeEvictionCallbacks();
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -444,5 +444,19 @@ public void NoCompactionWhenNoMaximumEntriesCountSpecified()
// There should be 6 items in the cache
Assert.Equal(6, cache.Count);
}

[Fact]
public void ClearZeroesTheSize()
{
var cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 10 });
Assert.Equal(0, cache.Size);

cache.Set("key", "value", new MemoryCacheEntryOptions { Size = 5 });
Assert.Equal(5, cache.Size);

cache.Clear();
Assert.Equal(0, cache.Size);
Assert.Equal(0, cache.Count);
}
}
}
Loading

0 comments on commit 092c2ab

Please sign in to comment.