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