1 // <copyright file="MemoryCacheStore.cs" company="Microsoft">
2 //   Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
3 // </copyright>
4 using System;
5 using System.Collections;
6 using System.Collections.Specialized;
7 using System.Threading;
8 using System.Diagnostics;
9 using System.Security;
10 using System.Security.Permissions;
11 using System.Diagnostics.CodeAnalysis;
12 
13 namespace System.Runtime.Caching {
14     internal sealed class MemoryCacheStore : IDisposable {
15         const int INSERT_BLOCK_WAIT = 10000;
16         const int MAX_COUNT = Int32.MaxValue / 2;
17 
18         private Hashtable _entries;
19         private Object _entriesLock;
20         private CacheExpires _expires;
21         private CacheUsage _usage;
22         private int _disposed;
23         private ManualResetEvent _insertBlock;
24         private volatile bool _useInsertBlock;
25         private MemoryCache _cache;
26         private PerfCounters _perfCounters;
27 
MemoryCacheStore(MemoryCache cache, PerfCounters perfCounters)28         internal MemoryCacheStore(MemoryCache cache, PerfCounters perfCounters) {
29             _cache = cache;
30             _perfCounters = perfCounters;
31             _entries = new Hashtable(new MemoryCacheEqualityComparer());
32             _entriesLock = new Object();
33             _expires = new CacheExpires(this);
34             _usage = new CacheUsage(this);
35             InitDisposableMembers();
36         }
37 
38         // private members
39 
AddToCache(MemoryCacheEntry entry)40         private void AddToCache(MemoryCacheEntry entry) {
41 
42             // add outside of lock
43             if (entry == null) {
44                 return;
45             }
46 
47             if (entry.HasExpiration()) {
48                 _expires.Add(entry);
49             }
50 
51             if (entry.HasUsage()
52                 && (!entry.HasExpiration() || entry.UtcAbsExp - DateTime.UtcNow >= CacheUsage.MIN_LIFETIME_FOR_USAGE)) {
53                 _usage.Add(entry);
54             }
55 
56             // One last sanity check to be sure we didn't fall victim to an Add ----
57             if (!entry.CompareExchangeState(EntryState.AddedToCache, EntryState.AddingToCache)) {
58                 if (entry.InExpires()) {
59                     _expires.Remove(entry);
60                 }
61 
62                 if (entry.InUsage()) {
63                     _usage.Remove(entry);
64                 }
65             }
66 
67             entry.CallNotifyOnChanged();
68             if (_perfCounters != null) {
69                 _perfCounters.Increment(PerfCounterName.Entries);
70                 _perfCounters.Increment(PerfCounterName.Turnover);
71             }
72         }
73 
InitDisposableMembers()74         private void InitDisposableMembers() {
75             _insertBlock = new ManualResetEvent(true);
76             _expires.EnableExpirationTimer(true);
77         }
78 
79 
RemoveFromCache(MemoryCacheEntry entry, CacheEntryRemovedReason reason, bool delayRelease = false)80         private void RemoveFromCache(MemoryCacheEntry entry, CacheEntryRemovedReason reason, bool delayRelease = false) {
81             // release outside of lock
82             if (entry != null) {
83                 if (entry.InExpires()) {
84                     _expires.Remove(entry);
85                 }
86 
87                 if (entry.InUsage()) {
88                     _usage.Remove(entry);
89                 }
90 
91                 Dbg.Assert(entry.State == EntryState.RemovingFromCache, "entry.State = EntryState.RemovingFromCache");
92 
93                 entry.State = EntryState.RemovedFromCache;
94                 if (!delayRelease) {
95                     entry.Release(_cache, reason);
96                 }
97                 if (_perfCounters != null) {
98                     _perfCounters.Decrement(PerfCounterName.Entries);
99                     _perfCounters.Increment(PerfCounterName.Turnover);
100                 }
101             }
102         }
103 
104         // 'updatePerfCounters' defaults to true since this method is called by all Get() operations
105         // to update both the performance counters and the sliding expiration. Callers that perform
106         // nested sliding expiration updates (like a MemoryCacheEntry touching its update sentinel)
107         // can pass false to prevent these from unintentionally showing up in the perf counters.
UpdateExpAndUsage(MemoryCacheEntry entry, bool updatePerfCounters = true)108         internal void UpdateExpAndUsage(MemoryCacheEntry entry, bool updatePerfCounters = true) {
109             if (entry != null) {
110                 if (entry.InUsage() || entry.SlidingExp > TimeSpan.Zero) {
111                     DateTime utcNow = DateTime.UtcNow;
112                     entry.UpdateSlidingExp(utcNow, _expires);
113                     entry.UpdateUsage(utcNow, _usage);
114                 }
115 
116                 // DevDiv #67021: If this entry has an update sentinel, the sliding expiration is actually associated
117                 // with that sentinel, not with this entry. We need to update the sentinel's sliding expiration to
118                 // keep the sentinel from expiring, which in turn would force a removal of this entry from the cache.
119                 entry.UpdateSlidingExpForUpdateSentinel();
120 
121                 if (updatePerfCounters && _perfCounters != null) {
122                     _perfCounters.Increment(PerfCounterName.Hits);
123                     _perfCounters.Increment(PerfCounterName.HitRatio);
124                     _perfCounters.Increment(PerfCounterName.HitRatioBase);
125                 }
126             }
127             else {
128                 if (updatePerfCounters && _perfCounters != null) {
129                     _perfCounters.Increment(PerfCounterName.Misses);
130                     _perfCounters.Increment(PerfCounterName.HitRatioBase);
131                 }
132             }
133         }
134 
WaitInsertBlock()135         private void WaitInsertBlock() {
136             _insertBlock.WaitOne(INSERT_BLOCK_WAIT, false);
137         }
138 
139 
140         // public/internal members
141 
142         internal CacheUsage Usage { get { return _usage; } }
143 
AddOrGetExisting(MemoryCacheKey key, MemoryCacheEntry entry)144         internal MemoryCacheEntry AddOrGetExisting(MemoryCacheKey key, MemoryCacheEntry entry) {
145             if (_useInsertBlock && entry.HasUsage()) {
146                 WaitInsertBlock();
147             }
148             MemoryCacheEntry existingEntry = null;
149             MemoryCacheEntry toBeReleasedEntry = null;
150             bool added = false;
151             lock (_entriesLock) {
152                 if (_disposed == 0) {
153                     existingEntry = _entries[key] as MemoryCacheEntry;
154                     // has it expired?
155                     if (existingEntry != null && existingEntry.UtcAbsExp <= DateTime.UtcNow) {
156                         toBeReleasedEntry = existingEntry;
157                         toBeReleasedEntry.State = EntryState.RemovingFromCache;
158                         existingEntry = null;
159                     }
160                     // can we add entry to the cache?
161                     if (existingEntry == null) {
162                         entry.State = EntryState.AddingToCache;
163                         added = true;
164                         _entries[key] = entry;
165                     }
166                 }
167             }
168             // release outside of lock
169             RemoveFromCache(toBeReleasedEntry, CacheEntryRemovedReason.Expired, delayRelease:true);
170             if (added) {
171                 // add outside of lock
172                 AddToCache(entry);
173             }
174             // update outside of lock
175             UpdateExpAndUsage(existingEntry);
176 
177             // Dev10 861163: Call Release after the new entry has been completely added so
178             // that the CacheItemRemovedCallback can take a dependency on the newly inserted item.
179             if (toBeReleasedEntry != null) {
180                 toBeReleasedEntry.Release(_cache, CacheEntryRemovedReason.Expired);
181             }
182             return existingEntry;
183         }
184 
BlockInsert()185         internal void BlockInsert() {
186             _insertBlock.Reset();
187             _useInsertBlock = true;
188         }
189 
CopyTo(IDictionary h)190         internal void CopyTo(IDictionary h) {
191             lock (_entriesLock) {
192                 if (_disposed == 0) {
193                     foreach (DictionaryEntry e in _entries) {
194                         MemoryCacheKey key = e.Key as MemoryCacheKey;
195                         MemoryCacheEntry entry = e.Value as MemoryCacheEntry;
196                         if (entry.UtcAbsExp > DateTime.UtcNow) {
197                             h[key.Key] = entry.Value;
198                         }
199                     }
200                 }
201             }
202         }
203 
204         internal int Count {
205             get {
206                 return _entries.Count;
207 
208             }
209         }
210 
Dispose()211         public void Dispose() {
212             if (Interlocked.Exchange(ref _disposed, 1) == 0) {
213                 // disable CacheExpires timer
214                 _expires.EnableExpirationTimer(false);
215                 // build array list of entries
216                 ArrayList entries = new ArrayList(_entries.Count);
217                 lock (_entriesLock) {
218                     foreach (DictionaryEntry e in _entries) {
219                         MemoryCacheEntry entry = e.Value as MemoryCacheEntry;
220                         entries.Add(entry);
221                     }
222                     foreach (MemoryCacheEntry entry in entries) {
223                         MemoryCacheKey key = entry as MemoryCacheKey;
224                         entry.State = EntryState.RemovingFromCache;
225                         _entries.Remove(key);
226                     }
227                 }
228                 // release entries outside of lock
229                 foreach (MemoryCacheEntry entry in entries) {
230                     RemoveFromCache(entry, CacheEntryRemovedReason.CacheSpecificEviction);
231                 }
232 
233                 // MemoryCacheStatistics has been disposed, and therefore nobody should be using
234                 // _insertBlock except for potential threads in WaitInsertBlock (which won't care if we call Close).
235                 Dbg.Assert(_useInsertBlock == false, "_useInsertBlock == false");
236                 _insertBlock.Close();
237 
238 
239                 // Don't need to call GC.SuppressFinalize(this) for sealed types without finalizers.
240             }
241         }
242 
Get(MemoryCacheKey key)243         internal MemoryCacheEntry Get(MemoryCacheKey key) {
244             MemoryCacheEntry entry = _entries[key] as MemoryCacheEntry;
245             // has it expired?
246             if (entry != null && entry.UtcAbsExp <= DateTime.UtcNow) {
247                 Remove(key, entry, CacheEntryRemovedReason.Expired);
248                 entry = null;
249             }
250             // update outside of lock
251             UpdateExpAndUsage(entry);
252             return entry;
253         }
254 
Remove(MemoryCacheKey key, MemoryCacheEntry entryToRemove, CacheEntryRemovedReason reason)255         internal MemoryCacheEntry Remove(MemoryCacheKey key, MemoryCacheEntry entryToRemove, CacheEntryRemovedReason reason) {
256             MemoryCacheEntry entry = null;
257             lock (_entriesLock) {
258                 if (_disposed == 0) {
259                     // get current entry
260                     entry = _entries[key] as MemoryCacheEntry;
261                     // remove if it matches the entry to be removed (but always remove if entryToRemove is null)
262                     if (entryToRemove == null || Object.ReferenceEquals(entry, entryToRemove)) {
263                         // Dev10 865887: MemoryCache.Remove("\ue637\ud22a\u3e17") causes NullReferenceEx
264                         if (entry != null) {
265                             entry.State = EntryState.RemovingFromCache;
266                             _entries.Remove(key);
267                         }
268                     }
269                     else {
270                         entry = null;
271                     }
272                 }
273             }
274             // release outside of lock
275             RemoveFromCache(entry, reason);
276             return entry;
277         }
278 
Set(MemoryCacheKey key, MemoryCacheEntry entry)279         internal void Set(MemoryCacheKey key, MemoryCacheEntry entry) {
280             if (_useInsertBlock && entry.HasUsage()) {
281                 WaitInsertBlock();
282             }
283             MemoryCacheEntry existingEntry = null;
284             bool added = false;
285             lock (_entriesLock) {
286                 if (_disposed == 0) {
287                     existingEntry = _entries[key] as MemoryCacheEntry;
288                     if (existingEntry != null) {
289                         existingEntry.State = EntryState.RemovingFromCache;
290                     }
291                     entry.State = EntryState.AddingToCache;
292                     added = true;
293                     _entries[key] = entry;
294                 }
295             }
296 
297             CacheEntryRemovedReason reason = CacheEntryRemovedReason.Removed;
298             if (existingEntry != null) {
299                 if (existingEntry.UtcAbsExp <= DateTime.UtcNow) {
300                     reason = CacheEntryRemovedReason.Expired;
301                 }
302                 RemoveFromCache(existingEntry, reason, delayRelease:true);
303             }
304             if (added) {
305                 AddToCache(entry);
306             }
307 
308             // Dev10 861163: Call Release after the new entry has been completely added so
309             // that the CacheItemRemovedCallback can take a dependency on the newly inserted item.
310             if (existingEntry != null) {
311                 existingEntry.Release(_cache, reason);
312             }
313         }
314 
TrimInternal(int percent)315         internal long TrimInternal(int percent) {
316             Dbg.Assert(percent <= 100, "percent <= 100");
317 
318             int count = Count;
319             int toTrim = 0;
320             // do we need to drop a percentage of entries?
321             if (percent > 0) {
322                 toTrim = (int)Math.Ceiling(((long)count * (long)percent) / 100D);
323                 // would this leave us above MAX_COUNT?
324                 int minTrim = count - MAX_COUNT;
325                 if (toTrim < minTrim) {
326                     toTrim = minTrim;
327                 }
328             }
329             // do we need to trim?
330             if (toTrim <= 0 || _disposed == 1) {
331                 return 0;
332             }
333             int trimmed = 0; // total number of entries trimmed
334             int trimmedOrExpired = 0;
335 #if DBG
336             int beginTotalCount = count;
337 #endif
338 
339             trimmedOrExpired = _expires.FlushExpiredItems(true);
340             if (trimmedOrExpired < toTrim) {
341                 trimmed = _usage.FlushUnderUsedItems(toTrim - trimmedOrExpired);
342                 trimmedOrExpired += trimmed;
343             }
344 
345             if (trimmed > 0 && _perfCounters != null) {
346                 // Update values for perfcounters
347                 _perfCounters.IncrementBy(PerfCounterName.Trims, trimmed);
348             }
349 
350 #if DBG
351             Dbg.Trace("MemoryCacheStore", "TrimInternal:"
352                         + " beginTotalCount=" + beginTotalCount
353                         + ", endTotalCount=" + count
354                         + ", percent=" + percent
355                         + ", trimmed=" + trimmed);
356 #endif
357             return trimmedOrExpired;
358 
359         }
360 
UnblockInsert()361         internal void UnblockInsert() {
362             _useInsertBlock = false;
363             _insertBlock.Set();
364         }
365     }
366 }
367