@@ -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 }
0 commit comments