1 // 2 // System.Web.Caching.Cache 3 // 4 // Author(s): 5 // Lluis Sanchez (lluis@ximian.com) 6 // Marek Habersack <mhabersack@novell.com> 7 // 8 // (C) 2005-2009 Novell, Inc (http://novell.com) 9 // 10 // 11 // Permission is hereby granted, free of charge, to any person obtaining 12 // a copy of this software and associated documentation files (the 13 // "Software"), to deal in the Software without restriction, including 14 // without limitation the rights to use, copy, modify, merge, publish, 15 // distribute, sublicense, and/or sell copies of the Software, and to 16 // permit persons to whom the Software is furnished to do so, subject to 17 // the following conditions: 18 // 19 // The above copyright notice and this permission notice shall be 20 // included in all copies or substantial portions of the Software. 21 // 22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 // 30 31 using System.Threading; 32 using System.Collections; 33 using System.Collections.Generic; 34 using System.Linq; 35 using System.Security.Permissions; 36 using System.Web.Configuration; 37 38 namespace System.Web.Caching 39 { 40 // CAS - no InheritanceDemand here as the class is sealed 41 [AspNetHostingPermission (SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] 42 public sealed class Cache: IEnumerable 43 { 44 const int LOW_WATER_MARK = 10000; // Target number of items if high water mark is reached 45 const int HIGH_WATER_MARK = 15000; // We start collection after exceeding this count 46 47 public static readonly DateTime NoAbsoluteExpiration = DateTime.MaxValue; 48 public static readonly TimeSpan NoSlidingExpiration = TimeSpan.Zero; 49 50 // cacheLock will be released in the code below without checking whether it was 51 // actually acquired. The API doesn't offer a reliable way to check whether the lock 52 // is being held by the current thread and since Mono does't implement CER 53 // (Constrained Execution Regions - 54 // http://msdn.microsoft.com/en-us/library/ms228973.aspx) currently, we have no 55 // reliable way of recording the information that the lock has been successfully 56 // acquired. 57 // It can happen that a Thread.Abort occurs while acquiring the lock and the lock 58 // isn't actually held. In this case the attempt to release a lock will throw an 59 // exception. It's better than a race of setting a boolean flag after acquiring the 60 // lock and then relying upon it here to release it - that may cause a deadlock 61 // should we fail to release the lock which was successfully acquired but 62 // Thread.Abort happened right after that during the stloc instruction to set the 63 // boolean flag. Once CERs are supported we can use the boolean flag reliably. 64 ReaderWriterLockSlim cacheLock; 65 CacheItemLRU cache; 66 CacheItemPriorityQueue timedItems; 67 Timer expirationTimer; 68 long expirationTimerPeriod = 0; 69 Cache dependencyCache; 70 bool? disableExpiration; 71 long privateBytesLimit = -1; 72 long percentagePhysicalMemoryLimit = -1; 73 74 bool DisableExpiration { 75 get { 76 if (disableExpiration == null) { 77 var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection; 78 if (cs == null) 79 disableExpiration = false; 80 else 81 disableExpiration = (bool)cs.DisableExpiration; 82 } 83 84 return (bool)disableExpiration; 85 } 86 } 87 88 public long EffectivePrivateBytesLimit { 89 get { 90 if (privateBytesLimit == -1) { 91 var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection; 92 if (cs == null) 93 privateBytesLimit = 0; 94 else 95 privateBytesLimit = cs.PrivateBytesLimit; 96 97 if (privateBytesLimit == 0) { 98 // http://blogs.msdn.com/tmarq/archive/2007/06/25/some-history-on-the-asp-net-cache-memory-limits.aspx 99 // TODO: calculate 100 privateBytesLimit = 734003200; 101 } 102 } 103 104 return privateBytesLimit; 105 } 106 } 107 108 public long EffectivePercentagePhysicalMemoryLimit { 109 get { 110 if (percentagePhysicalMemoryLimit == -1) { 111 var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection; 112 if (cs == null) 113 percentagePhysicalMemoryLimit = 0; 114 else 115 percentagePhysicalMemoryLimit = cs.PercentagePhysicalMemoryUsedLimit; 116 117 if (percentagePhysicalMemoryLimit == 0) { 118 // http://blogs.msdn.com/tmarq/archive/2007/06/25/some-history-on-the-asp-net-cache-memory-limits.aspx 119 // TODO: calculate 120 percentagePhysicalMemoryLimit = 97; 121 } 122 } 123 124 return percentagePhysicalMemoryLimit; 125 } 126 } 127 Cache()128 public Cache () 129 { 130 cacheLock = new ReaderWriterLockSlim (); 131 cache = new CacheItemLRU (this, HIGH_WATER_MARK, LOW_WATER_MARK); 132 } 133 134 public int Count { 135 get { return cache.Count; } 136 } 137 138 public object this [string key] { 139 get { return Get (key); } 140 set { Insert (key, value); } 141 } 142 143 // Must ALWAYS be called with the cache write lock held RemoveCacheItem(string key)144 CacheItem RemoveCacheItem (string key) 145 { 146 if (key == null) 147 return null; 148 149 CacheItem ret = cache [key]; 150 if (ret == null) 151 return null; 152 153 if (timedItems != null) 154 timedItems.OnItemDisable (ret); 155 156 ret.Disabled = true; 157 cache.Remove (key); 158 159 return ret; 160 } 161 Add(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)162 public object Add (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback) 163 { 164 if (key == null) 165 throw new ArgumentNullException ("key"); 166 167 try { 168 cacheLock.EnterWriteLock (); 169 CacheItem it = cache [key]; 170 171 if (it != null) 172 return it.Value; 173 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback, null, false); 174 } finally { 175 // See comment at the top of the file, above cacheLock declaration 176 cacheLock.ExitWriteLock (); 177 } 178 179 return null; 180 } 181 Get(string key)182 public object Get (string key) 183 { 184 try { 185 cacheLock.EnterUpgradeableReadLock (); 186 CacheItem it = cache [key]; 187 if (it == null) 188 return null; 189 190 if (it.Dependency != null && it.Dependency.HasChanged) { 191 try { 192 cacheLock.EnterWriteLock (); 193 if (!NeedsUpdate (it, CacheItemUpdateReason.DependencyChanged, false)) 194 Remove (it.Key, CacheItemRemovedReason.DependencyChanged, false, true); 195 } finally { 196 // See comment at the top of the file, above cacheLock declaration 197 cacheLock.ExitWriteLock (); 198 } 199 200 return null; 201 } 202 203 if (!DisableExpiration) { 204 if (it.SlidingExpiration != NoSlidingExpiration) { 205 it.AbsoluteExpiration = DateTime.Now + it.SlidingExpiration; 206 // Cast to long is ok since we know that sliding expiration 207 // is less than 365 days (31536000000ms) 208 long remaining = (long)it.SlidingExpiration.TotalMilliseconds; 209 it.ExpiresAt = it.AbsoluteExpiration.Ticks; 210 211 if (expirationTimer != null && (expirationTimerPeriod == 0 || expirationTimerPeriod > remaining)) { 212 expirationTimerPeriod = remaining; 213 expirationTimer.Change (expirationTimerPeriod, expirationTimerPeriod); 214 } 215 216 } else if (DateTime.Now >= it.AbsoluteExpiration) { 217 try { 218 cacheLock.EnterWriteLock (); 219 if (!NeedsUpdate (it, CacheItemUpdateReason.Expired, false)) 220 Remove (key, CacheItemRemovedReason.Expired, false, true); 221 } finally { 222 // See comment at the top of the file, above cacheLock declaration 223 cacheLock.ExitWriteLock (); 224 } 225 226 return null; 227 } 228 } 229 230 return it.Value; 231 } finally { 232 // See comment at the top of the file, above cacheLock declaration 233 cacheLock.ExitUpgradeableReadLock (); 234 } 235 } 236 Insert(string key, object value)237 public void Insert (string key, object value) 238 { 239 Insert (key, value, null, NoAbsoluteExpiration, NoSlidingExpiration, CacheItemPriority.Normal, null, null, true); 240 } 241 Insert(string key, object value, CacheDependency dependencies)242 public void Insert (string key, object value, CacheDependency dependencies) 243 { 244 Insert (key, value, dependencies, NoAbsoluteExpiration, NoSlidingExpiration, CacheItemPriority.Normal, null, null, true); 245 } 246 Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration)247 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration) 248 { 249 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, CacheItemPriority.Normal, null, null, true); 250 } 251 Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemUpdateCallback onUpdateCallback)252 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, 253 CacheItemUpdateCallback onUpdateCallback) 254 { 255 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, CacheItemPriority.Normal, null, onUpdateCallback, true); 256 } 257 Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)258 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, 259 CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback) 260 { 261 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback, null, true); 262 } 263 Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback, CacheItemUpdateCallback onUpdateCallback, bool doLock)264 void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, 265 CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback, CacheItemUpdateCallback onUpdateCallback, bool doLock) 266 { 267 if (key == null) 268 throw new ArgumentNullException ("key"); 269 if (value == null) 270 throw new ArgumentNullException ("value"); 271 if (slidingExpiration < TimeSpan.Zero || slidingExpiration > TimeSpan.FromDays (365)) 272 throw new ArgumentNullException ("slidingExpiration"); 273 if (absoluteExpiration != NoAbsoluteExpiration && slidingExpiration != NoSlidingExpiration) 274 throw new ArgumentException ("Both absoluteExpiration and slidingExpiration are specified"); 275 276 CacheItem ci = new CacheItem (); 277 ci.Value = value; 278 ci.Key = key; 279 280 if (dependencies != null) { 281 ci.Dependency = dependencies; 282 dependencies.DependencyChanged += new EventHandler (OnDependencyChanged); 283 dependencies.SetCache (DependencyCache); 284 } 285 286 ci.Priority = priority; 287 SetItemTimeout (ci, absoluteExpiration, slidingExpiration, onRemoveCallback, onUpdateCallback, key, doLock); 288 } 289 SetItemTimeout(string key, DateTime absoluteExpiration, TimeSpan slidingExpiration, bool doLock)290 internal void SetItemTimeout (string key, DateTime absoluteExpiration, TimeSpan slidingExpiration, bool doLock) 291 { 292 CacheItem ci = null; 293 try { 294 if (doLock) 295 cacheLock.EnterWriteLock (); 296 297 ci = cache [key]; 298 if (ci != null) 299 SetItemTimeout (ci, absoluteExpiration, slidingExpiration, ci.OnRemoveCallback, null, key, false); 300 } finally { 301 if (doLock) { 302 // See comment at the top of the file, above cacheLock declaration 303 cacheLock.ExitWriteLock (); 304 } 305 } 306 } 307 SetItemTimeout(CacheItem ci, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemRemovedCallback onRemoveCallback, CacheItemUpdateCallback onUpdateCallback, string key, bool doLock)308 void SetItemTimeout (CacheItem ci, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemRemovedCallback onRemoveCallback, 309 CacheItemUpdateCallback onUpdateCallback, string key, bool doLock) 310 { 311 bool disableExpiration = DisableExpiration; 312 313 if (!disableExpiration) { 314 ci.SlidingExpiration = slidingExpiration; 315 if (slidingExpiration != NoSlidingExpiration) 316 ci.AbsoluteExpiration = DateTime.Now + slidingExpiration; 317 else 318 ci.AbsoluteExpiration = absoluteExpiration; 319 } 320 321 ci.OnRemoveCallback = onRemoveCallback; 322 ci.OnUpdateCallback = onUpdateCallback; 323 324 try { 325 if (doLock) 326 cacheLock.EnterWriteLock (); 327 328 if (key != null) { 329 cache [key] = ci; 330 cache.EvictIfNecessary (); 331 } 332 333 ci.LastChange = DateTime.Now; 334 if (!disableExpiration && ci.AbsoluteExpiration != NoAbsoluteExpiration) { 335 bool enqueue; 336 if (ci.IsTimedItem) { 337 enqueue = UpdateTimedItem (ci); 338 if (!enqueue) 339 UpdateTimerPeriod (ci); 340 } else 341 enqueue = true; 342 343 if (enqueue) { 344 ci.IsTimedItem = true; 345 EnqueueTimedItem (ci); 346 } 347 348 } 349 } finally { 350 if (doLock) { 351 // See comment at the top of the file, above cacheLock declaration 352 cacheLock.ExitWriteLock (); 353 } 354 } 355 } 356 357 // MUST be called with cache lock held UpdateTimedItem(CacheItem item)358 bool UpdateTimedItem (CacheItem item) 359 { 360 if (timedItems == null) 361 return true; 362 363 item.ExpiresAt = item.AbsoluteExpiration.Ticks; 364 return !timedItems.Update (item); 365 } 366 367 // MUST be called with cache lock held UpdateTimerPeriod(CacheItem item)368 void UpdateTimerPeriod (CacheItem item) 369 { 370 if (timedItems == null) 371 timedItems = new CacheItemPriorityQueue (); 372 373 long remaining = Math.Max (0, (long)(item.AbsoluteExpiration - DateTime.Now).TotalMilliseconds); 374 item.ExpiresAt = item.AbsoluteExpiration.Ticks; 375 376 if (remaining > 4294967294) 377 // Maximum due time for timer 378 // Item will expire properly anyway, as the timer will be 379 // rescheduled for the item's expiration time once that item is 380 // bubbled to the top of the priority queue. 381 remaining = 4294967294; 382 383 if (expirationTimer != null && expirationTimerPeriod <= remaining) 384 return; 385 expirationTimerPeriod = remaining; 386 387 if (expirationTimer == null) 388 expirationTimer = new Timer (new TimerCallback (ExpireItems), null, expirationTimerPeriod, expirationTimerPeriod); 389 else 390 expirationTimer.Change (expirationTimerPeriod, expirationTimerPeriod); 391 } 392 393 // MUST be called with cache lock held EnqueueTimedItem(CacheItem item)394 void EnqueueTimedItem (CacheItem item) 395 { 396 UpdateTimerPeriod (item); 397 timedItems.Enqueue (item); 398 } 399 Remove(string key)400 public object Remove (string key) 401 { 402 return Remove (key, CacheItemRemovedReason.Removed, true, true); 403 } 404 Remove(string key, CacheItemRemovedReason reason, bool doLock, bool invokeCallback)405 internal object Remove (string key, CacheItemRemovedReason reason, bool doLock, bool invokeCallback) 406 { 407 CacheItem it = null; 408 try { 409 if (doLock) 410 cacheLock.EnterWriteLock (); 411 412 it = RemoveCacheItem (key); 413 } finally { 414 if (doLock) { 415 // See comment at the top of the file, above cacheLock declaration 416 cacheLock.ExitWriteLock (); 417 } 418 } 419 420 object ret = null; 421 if (it != null) { 422 if (it.Dependency != null) { 423 it.Dependency.SetCache (null); 424 it.Dependency.DependencyChanged -= new EventHandler (OnDependencyChanged); 425 it.Dependency.Dispose (); 426 } 427 if (invokeCallback && it.OnRemoveCallback != null) { 428 try { 429 it.OnRemoveCallback (key, it.Value, reason); 430 } catch { 431 //TODO: anything to be done here? 432 } 433 } 434 ret = it.Value; 435 it.Value = null; 436 it.Key = null; 437 it.Dependency = null; 438 it.OnRemoveCallback = null; 439 it.OnUpdateCallback = null; 440 it = null; 441 } 442 443 return ret; 444 } 445 446 // Used when shutting down the application so that 447 // session_end events are sent for all sessions. InvokePrivateCallbacks()448 internal void InvokePrivateCallbacks () 449 { 450 try { 451 cacheLock.EnterReadLock (); 452 cache.InvokePrivateCallbacks (); 453 } finally { 454 // See comment at the top of the file, above cacheLock declaration 455 cacheLock.ExitReadLock (); 456 } 457 } 458 GetEnumerator()459 public IDictionaryEnumerator GetEnumerator () 460 { 461 List <CacheItem> list = null; 462 try { 463 cacheLock.EnterReadLock (); 464 list = cache.ToList (); 465 } finally { 466 // See comment at the top of the file, above cacheLock declaration 467 cacheLock.ExitReadLock (); 468 } 469 470 return new CacheItemEnumerator (list); 471 } 472 IEnumerable.GetEnumerator()473 IEnumerator IEnumerable.GetEnumerator () 474 { 475 return GetEnumerator (); 476 } 477 OnDependencyChanged(object o, EventArgs a)478 void OnDependencyChanged (object o, EventArgs a) 479 { 480 CheckDependencies (); 481 } 482 NeedsUpdate(CacheItem item, CacheItemUpdateReason reason, bool needLock)483 bool NeedsUpdate (CacheItem item, CacheItemUpdateReason reason, bool needLock) 484 { 485 try { 486 if (needLock) 487 cacheLock.EnterWriteLock (); 488 489 if (item == null || item.OnUpdateCallback == null) 490 return false; 491 492 object expensiveObject; 493 CacheDependency dependency; 494 DateTime absoluteExpiration; 495 TimeSpan slidingExpiration; 496 string key = item.Key; 497 CacheItemUpdateCallback updateCB = item.OnUpdateCallback; 498 499 updateCB (key, reason, out expensiveObject, out dependency, out absoluteExpiration, out slidingExpiration); 500 if (expensiveObject == null) 501 return false; 502 503 CacheItemPriority priority = item.Priority; 504 CacheItemRemovedCallback removeCB = item.OnRemoveCallback; 505 CacheItemRemovedReason whyRemoved; 506 507 switch (reason) { 508 case CacheItemUpdateReason.Expired: 509 whyRemoved = CacheItemRemovedReason.Expired; 510 break; 511 512 case CacheItemUpdateReason.DependencyChanged: 513 whyRemoved = CacheItemRemovedReason.DependencyChanged; 514 break; 515 516 default: 517 whyRemoved = CacheItemRemovedReason.Removed; 518 break; 519 } 520 521 Remove (key, whyRemoved, false, false); 522 Insert (key, expensiveObject, dependency, absoluteExpiration, slidingExpiration, priority, removeCB, updateCB, false); 523 524 return true; 525 } catch (Exception) { 526 return false; 527 } finally { 528 if (needLock) { 529 // See comment at the top of the file, above cacheLock declaration 530 cacheLock.ExitWriteLock (); 531 } 532 } 533 } 534 ExpireItems(object data)535 void ExpireItems (object data) 536 { 537 DateTime now = DateTime.Now; 538 CacheItem item = null; 539 540 expirationTimer.Change (Timeout.Infinite, Timeout.Infinite); 541 try { 542 cacheLock.EnterWriteLock (); 543 while (true) { 544 item = timedItems.Peek (); 545 546 if (item == null) { 547 if (timedItems.Count == 0) 548 break; 549 550 timedItems.Dequeue (); 551 continue; 552 } 553 554 if (!item.Disabled && item.ExpiresAt > now.Ticks) 555 break; 556 557 if (item.Disabled) { 558 item = timedItems.Dequeue (); 559 continue; 560 } 561 562 item = timedItems.Dequeue (); 563 if (item != null) 564 if (!NeedsUpdate (item, CacheItemUpdateReason.Expired, false)) 565 Remove (item.Key, CacheItemRemovedReason.Expired, false, true); 566 } 567 } finally { 568 // See comment at the top of the file, above cacheLock declaration 569 cacheLock.ExitWriteLock (); 570 } 571 572 if (item != null) { 573 long remaining = Math.Max (0, (long)(item.AbsoluteExpiration - now).TotalMilliseconds); 574 if (remaining > 0 && (expirationTimerPeriod == 0 || expirationTimerPeriod > remaining)) { 575 expirationTimerPeriod = remaining; 576 expirationTimer.Change (expirationTimerPeriod, expirationTimerPeriod); 577 return; 578 } 579 if (expirationTimerPeriod > 0) 580 return; 581 } 582 583 expirationTimer.Change (Timeout.Infinite, Timeout.Infinite); 584 expirationTimerPeriod = 0; 585 } 586 CheckDependencies()587 internal void CheckDependencies () 588 { 589 try { 590 cacheLock.EnterWriteLock (); 591 List <CacheItem> list = cache.SelectItems (it => { 592 if (it == null) 593 return false; 594 if (it.Dependency != null && it.Dependency.HasChanged && !NeedsUpdate (it, CacheItemUpdateReason.DependencyChanged, false)) 595 return true; 596 return false; 597 }); 598 599 foreach (CacheItem it in list) 600 Remove (it.Key, CacheItemRemovedReason.DependencyChanged, false, true); 601 list.Clear (); 602 list.TrimExcess (); 603 } finally { 604 // See comment at the top of the file, above cacheLock declaration 605 cacheLock.ExitWriteLock (); 606 } 607 } 608 GetKeyLastChange(string key)609 internal DateTime GetKeyLastChange (string key) 610 { 611 try { 612 cacheLock.EnterReadLock (); 613 CacheItem it = cache [key]; 614 615 if (it == null) 616 return DateTime.MaxValue; 617 618 return it.LastChange; 619 } finally { 620 // See comment at the top of the file, above cacheLock declaration 621 cacheLock.ExitReadLock (); 622 } 623 } 624 625 internal Cache DependencyCache { 626 get { 627 if (dependencyCache == null) 628 return this; 629 630 return dependencyCache; 631 } 632 set { dependencyCache = value; } 633 } 634 } 635 } 636