Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 21c850a

Browse files
committed
Update ConcurrentDictionary implementation for MemoryCache
1 parent 5294a1e commit 21c850a

File tree

6 files changed

+138
-119
lines changed

6 files changed

+138
-119
lines changed

src/Microsoft.Extensions.Caching.Memory/CacheEntry.cs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
#if NETSTANDARD1_3
5-
#else
6-
using System.Runtime.Remoting;
7-
using System.Runtime.Remoting.Messaging;
8-
#endif
9-
104
using System;
115
using System.Collections.Generic;
126
using System.Threading;
@@ -22,7 +16,6 @@ internal class CacheEntry : ICacheEntry
2216
private readonly Action<CacheEntry> _notifyCacheOfExpiration;
2317
private readonly Action<CacheEntry> _notifyCacheEntryDisposed;
2418
private IList<IDisposable> _expirationTokenRegistrations;
25-
private EvictionReason _evictionReason;
2619
private IList<PostEvictionCallbackRegistration> _postEvictionCallbacks;
2720
private bool _isExpired;
2821

@@ -166,6 +159,8 @@ public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks
166159

167160
internal DateTimeOffset LastAccessed { get; set; }
168161

162+
internal EvictionReason EvictionReason { get; private set; }
163+
169164
public void Dispose()
170165
{
171166
if (!_added)
@@ -184,11 +179,11 @@ internal bool CheckExpired(DateTimeOffset now)
184179

185180
internal void SetExpired(EvictionReason reason)
186181
{
187-
_isExpired = true;
188-
if (_evictionReason == EvictionReason.None)
182+
if (EvictionReason == EvictionReason.None)
189183
{
190-
_evictionReason = reason;
184+
EvictionReason = reason;
191185
}
186+
_isExpired = true;
192187
DetachTokens();
193188
}
194189

@@ -302,7 +297,7 @@ private static void InvokeCallbacks(CacheEntry entry)
302297

303298
try
304299
{
305-
registration.EvictionCallback?.Invoke(entry.Key, entry.Value, entry._evictionReason, registration.State);
300+
registration.EvictionCallback?.Invoke(entry.Key, entry.Value, entry.EvictionReason, registration.State);
306301
}
307302
catch (Exception)
308303
{

src/Microsoft.Extensions.Caching.Memory/MemoryCache.cs

Lines changed: 88 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ public int Count
7272
get { return _entries.Count; }
7373
}
7474

75+
private ICollection<KeyValuePair<object, CacheEntry>> EntriesCollection => _entries;
76+
7577
/// <inheritdoc />
7678
public ICacheEntry CreateEntry(object key)
7779
{
@@ -86,6 +88,12 @@ public ICacheEntry CreateEntry(object key)
8688

8789
private void SetEntry(CacheEntry entry)
8890
{
91+
if (_disposed)
92+
{
93+
// No-op instead of throwing since this is called during CacheEntry.Dispose
94+
return;
95+
}
96+
8997
var utcNow = _clock.UtcNow;
9098

9199
DateTimeOffset? absoluteExpiration = null;
@@ -112,31 +120,57 @@ private void SetEntry(CacheEntry entry)
112120
// Initialize the last access timestamp at the time the entry is added
113121
entry.LastAccessed = utcNow;
114122

115-
var added = false;
116123
CacheEntry priorEntry;
117-
118-
if (_entries.TryRemove(entry.Key, out priorEntry))
124+
if (_entries.TryGetValue(entry.Key, out priorEntry))
119125
{
120126
priorEntry.SetExpired(EvictionReason.Replaced);
121127
}
122128

123129
if (!entry.CheckExpired(utcNow))
124130
{
125-
if (_entries.TryAdd(entry.Key, entry))
131+
var entryAdded = false;
132+
133+
if (priorEntry == null)
134+
{
135+
// Try to add the new entry if no previous entries exist.
136+
entryAdded = _entries.TryAdd(entry.Key, entry);
137+
}
138+
else
139+
{
140+
// Try to update with the new entry if a previous entries exist.
141+
entryAdded = _entries.TryUpdate(entry.Key, entry, priorEntry);
142+
143+
if (!entryAdded)
144+
{
145+
// The update will fail if the previous entry was removed after retrival.
146+
// Adding the new entry will succeed only if no entry has been added since.
147+
// This guarantees removing an old entry does not prevent adding a new entry.
148+
entryAdded = _entries.TryAdd(entry.Key, entry);
149+
}
150+
}
151+
152+
if (entryAdded)
126153
{
127154
entry.AttachTokens();
128-
added = true;
129155
}
130-
}
156+
else
157+
{
158+
entry.SetExpired(EvictionReason.Replaced);
159+
entry.InvokeEvictionCallbacks();
160+
}
131161

132-
if (priorEntry != null)
133-
{
134-
priorEntry.InvokeEvictionCallbacks();
162+
if (priorEntry != null)
163+
{
164+
priorEntry.InvokeEvictionCallbacks();
165+
}
135166
}
136-
137-
if (!added)
167+
else
138168
{
139169
entry.InvokeEvictionCallbacks();
170+
if (priorEntry != null)
171+
{
172+
RemoveEntry(priorEntry);
173+
}
140174
}
141175

142176
StartScanForExpiredItems();
@@ -150,18 +184,21 @@ public bool TryGetValue(object key, out object result)
150184
throw new ArgumentNullException(nameof(key));
151185
}
152186

153-
var utcNow = _clock.UtcNow;
154-
result = null;
155-
bool found = false;
156-
CacheEntry expiredEntry = null;
157187
CheckDisposed();
188+
189+
result = null;
190+
var utcNow = _clock.UtcNow;
191+
var found = false;
192+
158193
CacheEntry entry;
159194
if (_entries.TryGetValue(key, out entry))
160195
{
161196
// Check if expired due to expiration tokens, timers, etc. and if so, remove it.
162-
if (entry.CheckExpired(utcNow))
197+
// Allow a stale Replaced value to be returned due to a race with SetExpired during SetEntry.
198+
if (entry.CheckExpired(utcNow) && entry.EvictionReason != EvictionReason.Replaced)
163199
{
164-
expiredEntry = entry;
200+
// TODO: For efficiency queue this up for batch removal
201+
RemoveEntry(entry);
165202
}
166203
else
167204
{
@@ -175,12 +212,6 @@ public bool TryGetValue(object key, out object result)
175212
}
176213
}
177214

178-
if (expiredEntry != null)
179-
{
180-
// TODO: For efficiency queue this up for batch removal
181-
RemoveEntry(expiredEntry);
182-
}
183-
184215
StartScanForExpiredItems();
185216

186217
return found;
@@ -196,46 +227,18 @@ public void Remove(object key)
196227

197228
CheckDisposed();
198229
CacheEntry entry;
199-
if (_entries.TryGetValue(key, out entry))
230+
if (_entries.TryRemove(key, out entry))
200231
{
201232
entry.SetExpired(EvictionReason.Removed);
202-
}
203-
204-
if (entry != null)
205-
{
206-
// TODO: For efficiency consider processing these removals in batches.
207-
RemoveEntry(entry);
233+
entry.InvokeEvictionCallbacks();
208234
}
209235

210236
StartScanForExpiredItems();
211237
}
212238

213239
private void RemoveEntry(CacheEntry entry)
214240
{
215-
// Only remove it if someone hasn't modified it since our lookup
216-
CacheEntry currentEntry;
217-
if (_entries.TryGetValue(entry.Key, out currentEntry)
218-
&& object.ReferenceEquals(currentEntry, entry))
219-
{
220-
_entries.TryRemove(entry.Key, out currentEntry);
221-
}
222-
entry.InvokeEvictionCallbacks();
223-
}
224-
225-
private void RemoveEntries(List<CacheEntry> entries)
226-
{
227-
foreach (var entry in entries)
228-
{
229-
// Only remove it if someone hasn't modified it since our lookup
230-
CacheEntry currentEntry;
231-
if (_entries.TryGetValue(entry.Key, out currentEntry)
232-
&& object.ReferenceEquals(currentEntry, entry))
233-
{
234-
_entries.TryRemove(entry.Key, out currentEntry);
235-
}
236-
}
237-
238-
foreach (var entry in entries)
241+
if (EntriesCollection.Remove(new KeyValuePair<object, CacheEntry>(entry.Key, entry)))
239242
{
240243
entry.InvokeEvictionCallbacks();
241244
}
@@ -263,18 +266,14 @@ private void StartScanForExpiredItems()
263266

264267
private static void ScanForExpiredItems(MemoryCache cache)
265268
{
266-
List<CacheEntry> expiredEntries = new List<CacheEntry>();
267-
268269
var now = cache._clock.UtcNow;
269-
foreach (var entry in cache._entries)
270+
foreach (var entry in cache._entries.Values)
270271
{
271-
if (entry.Value.CheckExpired(now))
272+
if (entry.CheckExpired(now))
272273
{
273-
expiredEntries.Add(entry.Value);
274+
cache.RemoveEntry(entry);
274275
}
275276
}
276-
277-
cache.RemoveEntries(expiredEntries);
278277
}
279278

280279
/// This is called after a Gen2 garbage collection. We assume this means there was memory pressure.
@@ -294,80 +293,79 @@ private bool DoMemoryPreassureCollection(object state)
294293
/// Remove at least the given percentage (0.10 for 10%) of the total entries (or estimated memory?), according to the following policy:
295294
/// 1. Remove all expired items.
296295
/// 2. Bucket by CacheItemPriority.
297-
/// ?. Least recently used objects.
296+
/// 3. Least recently used objects.
298297
/// ?. Items with the soonest absolute expiration.
299298
/// ?. Items with the soonest sliding expiration.
300299
/// ?. Larger objects - estimated by object graph size, inaccurate.
301300
public void Compact(double percentage)
302301
{
303-
List<CacheEntry> expiredEntries = new List<CacheEntry>();
304-
List<CacheEntry> lowPriEntries = new List<CacheEntry>();
305-
List<CacheEntry> normalPriEntries = new List<CacheEntry>();
306-
List<CacheEntry> highPriEntries = new List<CacheEntry>();
307-
List<CacheEntry> neverRemovePriEntries = new List<CacheEntry>();
302+
var entriesToRemove = new List<CacheEntry>();
303+
var lowPriEntries = new List<CacheEntry>();
304+
var normalPriEntries = new List<CacheEntry>();
305+
var highPriEntries = new List<CacheEntry>();
308306

309307
// Sort items by expired & priority status
310308
var now = _clock.UtcNow;
311-
foreach (var entry in _entries)
309+
foreach (var entry in _entries.Values)
312310
{
313-
if (entry.Value.CheckExpired(now))
311+
if (entry.CheckExpired(now))
314312
{
315-
expiredEntries.Add(entry.Value);
313+
entriesToRemove.Add(entry);
316314
}
317315
else
318316
{
319-
switch (entry.Value.Priority)
317+
switch (entry.Priority)
320318
{
321319
case CacheItemPriority.Low:
322-
lowPriEntries.Add(entry.Value);
320+
lowPriEntries.Add(entry);
323321
break;
324322
case CacheItemPriority.Normal:
325-
normalPriEntries.Add(entry.Value);
323+
normalPriEntries.Add(entry);
326324
break;
327325
case CacheItemPriority.High:
328-
highPriEntries.Add(entry.Value);
326+
highPriEntries.Add(entry);
329327
break;
330328
case CacheItemPriority.NeverRemove:
331-
neverRemovePriEntries.Add(entry.Value);
332329
break;
333330
default:
334-
System.Diagnostics.Debug.Assert(false, "Not implemented: " + entry.Value.Priority);
335-
break;
331+
throw new NotSupportedException("Not implemented: " + entry.Priority);
336332
}
337333
}
338334
}
339335

340-
int totalEntries = expiredEntries.Count + lowPriEntries.Count + normalPriEntries.Count + highPriEntries.Count + neverRemovePriEntries.Count;
341-
int removalCountTarget = (int)(totalEntries * percentage);
336+
int removalCountTarget = (int)(_entries.Count * percentage);
342337

343-
ExpirePriorityBucket(removalCountTarget, expiredEntries, lowPriEntries);
344-
ExpirePriorityBucket(removalCountTarget, expiredEntries, normalPriEntries);
345-
ExpirePriorityBucket(removalCountTarget, expiredEntries, highPriEntries);
338+
ExpirePriorityBucket(removalCountTarget, entriesToRemove, lowPriEntries);
339+
ExpirePriorityBucket(removalCountTarget, entriesToRemove, normalPriEntries);
340+
ExpirePriorityBucket(removalCountTarget, entriesToRemove, highPriEntries);
346341

347-
RemoveEntries(expiredEntries);
342+
foreach (var entry in entriesToRemove)
343+
{
344+
RemoveEntry(entry);
345+
}
348346
}
349347

350348
/// Policy:
351-
/// ?. Least recently used objects.
349+
/// 1. Least recently used objects.
352350
/// ?. Items with the soonest absolute expiration.
353351
/// ?. Items with the soonest sliding expiration.
354352
/// ?. Larger objects - estimated by object graph size, inaccurate.
355-
private void ExpirePriorityBucket(int removalCountTarget, List<CacheEntry> expiredEntries, List<CacheEntry> priorityEntries)
353+
private void ExpirePriorityBucket(int removalCountTarget, List<CacheEntry> entriesToRemove, List<CacheEntry> priorityEntries)
356354
{
357355
// Do we meet our quota by just removing expired entries?
358-
if (removalCountTarget <= expiredEntries.Count)
356+
if (removalCountTarget <= entriesToRemove.Count)
359357
{
360358
// No-op, we've met quota
361359
return;
362360
}
363-
if (expiredEntries.Count + priorityEntries.Count <= removalCountTarget)
361+
if (entriesToRemove.Count + priorityEntries.Count <= removalCountTarget)
364362
{
365363
// Expire all of the entries in this bucket
366364
foreach (var entry in priorityEntries)
367365
{
368366
entry.SetExpired(EvictionReason.Capacity);
369367
}
370-
expiredEntries.AddRange(priorityEntries);
368+
entriesToRemove.AddRange(priorityEntries);
371369
return;
372370
}
373371

@@ -378,8 +376,8 @@ private void ExpirePriorityBucket(int removalCountTarget, List<CacheEntry> expir
378376
foreach (var entry in priorityEntries.OrderBy(entry => entry.LastAccessed))
379377
{
380378
entry.SetExpired(EvictionReason.Capacity);
381-
expiredEntries.Add(entry);
382-
if (removalCountTarget <= expiredEntries.Count)
379+
entriesToRemove.Add(entry);
380+
if (removalCountTarget <= entriesToRemove.Count)
383381
{
384382
break;
385383
}

test/Microsoft.Extensions.Caching.Memory.Tests/CacheEntryScopeExpirationTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,8 @@ public void LinkContextsCanNest()
376376

377377
Assert.Equal(1, ((CacheEntry)entry1)._expirationTokens.Count());
378378
Assert.Null(((CacheEntry)entry1)._absoluteExpiration);
379-
Assert.Equal(1, ((CacheEntry)entry1)._expirationTokens.Count());
380-
Assert.Null(((CacheEntry)entry1)._absoluteExpiration);
379+
Assert.Equal(1, ((CacheEntry)entry)._expirationTokens.Count());
380+
Assert.Null(((CacheEntry)entry)._absoluteExpiration);
381381
}
382382

383383
[Fact]

0 commit comments

Comments
 (0)