2 // System.Web.Caching.Cache
5 // Lluis Sanchez (lluis@ximian.com)
6 // Marek Habersack <mhabersack@novell.com>
8 // (C) 2005-2009 Novell, Inc (http://novell.com)
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:
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
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.
31 using System.Threading;
32 using System.Collections;
33 using System.Collections.Generic;
35 using System.Security.Permissions;
36 using System.Web.Configuration;
38 namespace System.Web.Caching
40 // CAS - no InheritanceDemand here as the class is sealed
41 [AspNetHostingPermission (SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
42 public sealed class Cache: IEnumerable
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
47 public static readonly DateTime NoAbsoluteExpiration = DateTime.MaxValue;
48 public static readonly TimeSpan NoSlidingExpiration = TimeSpan.Zero;
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
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;
66 CacheItemPriorityQueue timedItems;
67 Timer expirationTimer;
68 long expirationTimerPeriod = 0;
69 Cache dependencyCache;
70 bool? disableExpiration;
71 long privateBytesLimit = -1;
72 long percentagePhysicalMemoryLimit = -1;
74 bool DisableExpiration {
76 if (disableExpiration == null) {
77 var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection;
79 disableExpiration = false;
81 disableExpiration = (bool)cs.DisableExpiration;
84 return (bool)disableExpiration;
88 public long EffectivePrivateBytesLimit {
90 if (privateBytesLimit == -1) {
91 var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection;
93 privateBytesLimit = 0;
95 privateBytesLimit = cs.PrivateBytesLimit;
97 if (privateBytesLimit == 0) {
98 // http://blogs.msdn.com/tmarq/archive/2007/06/25/some-history-on-the-asp-net-cache-memory-limits.aspx
100 privateBytesLimit = 734003200;
104 return privateBytesLimit;
108 public long EffectivePercentagePhysicalMemoryLimit {
110 if (percentagePhysicalMemoryLimit == -1) {
111 var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection;
113 percentagePhysicalMemoryLimit = 0;
115 percentagePhysicalMemoryLimit = cs.PercentagePhysicalMemoryUsedLimit;
117 if (percentagePhysicalMemoryLimit == 0) {
118 // http://blogs.msdn.com/tmarq/archive/2007/06/25/some-history-on-the-asp-net-cache-memory-limits.aspx
120 percentagePhysicalMemoryLimit = 97;
124 return percentagePhysicalMemoryLimit;
130 cacheLock = new ReaderWriterLockSlim ();
131 cache = new CacheItemLRU (this, HIGH_WATER_MARK, LOW_WATER_MARK);
135 get { return cache.Count; }
138 public object this [string key] {
139 get { return Get (key); }
140 set { Insert (key, value); }
143 // Must ALWAYS be called with the cache write lock held
144 CacheItem RemoveCacheItem (string key)
149 CacheItem ret = cache [key];
153 if (timedItems != null)
154 timedItems.OnItemDisable (ret);
162 public object Add (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)
165 throw new ArgumentNullException ("key");
168 cacheLock.EnterWriteLock ();
169 CacheItem it = cache [key];
173 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback, null, false);
175 // See comment at the top of the file, above cacheLock declaration
176 cacheLock.ExitWriteLock ();
182 public object Get (string key)
185 cacheLock.EnterUpgradeableReadLock ();
186 CacheItem it = cache [key];
190 if (it.Dependency != null && it.Dependency.HasChanged) {
192 cacheLock.EnterWriteLock ();
193 if (!NeedsUpdate (it, CacheItemUpdateReason.DependencyChanged, false))
194 Remove (it.Key, CacheItemRemovedReason.DependencyChanged, false, true);
196 // See comment at the top of the file, above cacheLock declaration
197 cacheLock.ExitWriteLock ();
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;
211 if (expirationTimer != null && (expirationTimerPeriod == 0 || expirationTimerPeriod > remaining)) {
212 expirationTimerPeriod = remaining;
213 expirationTimer.Change (expirationTimerPeriod, expirationTimerPeriod);
216 } else if (DateTime.Now >= it.AbsoluteExpiration) {
218 cacheLock.EnterWriteLock ();
219 if (!NeedsUpdate (it, CacheItemUpdateReason.Expired, false))
220 Remove (key, CacheItemRemovedReason.Expired, false, true);
222 // See comment at the top of the file, above cacheLock declaration
223 cacheLock.ExitWriteLock ();
232 // See comment at the top of the file, above cacheLock declaration
233 cacheLock.ExitUpgradeableReadLock ();
237 public void Insert (string key, object value)
239 Insert (key, value, null, NoAbsoluteExpiration, NoSlidingExpiration, CacheItemPriority.Normal, null, null, true);
242 public void Insert (string key, object value, CacheDependency dependencies)
244 Insert (key, value, dependencies, NoAbsoluteExpiration, NoSlidingExpiration, CacheItemPriority.Normal, null, null, true);
247 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration)
249 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, CacheItemPriority.Normal, null, null, true);
252 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration,
253 CacheItemUpdateCallback onUpdateCallback)
255 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, CacheItemPriority.Normal, null, onUpdateCallback, true);
258 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration,
259 CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)
261 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback, null, true);
264 void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration,
265 CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback, CacheItemUpdateCallback onUpdateCallback, bool doLock)
268 throw new ArgumentNullException ("key");
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");
276 CacheItem ci = new CacheItem ();
280 if (dependencies != null) {
281 ci.Dependency = dependencies;
282 dependencies.DependencyChanged += new EventHandler (OnDependencyChanged);
283 dependencies.SetCache (DependencyCache);
286 ci.Priority = priority;
287 SetItemTimeout (ci, absoluteExpiration, slidingExpiration, onRemoveCallback, onUpdateCallback, key, doLock);
290 internal void SetItemTimeout (string key, DateTime absoluteExpiration, TimeSpan slidingExpiration, bool doLock)
295 cacheLock.EnterWriteLock ();
299 SetItemTimeout (ci, absoluteExpiration, slidingExpiration, ci.OnRemoveCallback, null, key, false);
302 // See comment at the top of the file, above cacheLock declaration
303 cacheLock.ExitWriteLock ();
308 void SetItemTimeout (CacheItem ci, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemRemovedCallback onRemoveCallback,
309 CacheItemUpdateCallback onUpdateCallback, string key, bool doLock)
311 bool disableExpiration = DisableExpiration;
313 if (!disableExpiration) {
314 ci.SlidingExpiration = slidingExpiration;
315 if (slidingExpiration != NoSlidingExpiration)
316 ci.AbsoluteExpiration = DateTime.Now + slidingExpiration;
318 ci.AbsoluteExpiration = absoluteExpiration;
321 ci.OnRemoveCallback = onRemoveCallback;
322 ci.OnUpdateCallback = onUpdateCallback;
326 cacheLock.EnterWriteLock ();
330 cache.EvictIfNecessary ();
333 ci.LastChange = DateTime.Now;
334 if (!disableExpiration && ci.AbsoluteExpiration != NoAbsoluteExpiration) {
336 if (ci.IsTimedItem) {
337 enqueue = UpdateTimedItem (ci);
339 UpdateTimerPeriod (ci);
344 ci.IsTimedItem = true;
345 EnqueueTimedItem (ci);
351 // See comment at the top of the file, above cacheLock declaration
352 cacheLock.ExitWriteLock ();
357 // MUST be called with cache lock held
358 bool UpdateTimedItem (CacheItem item)
360 if (timedItems == null)
363 item.ExpiresAt = item.AbsoluteExpiration.Ticks;
364 return !timedItems.Update (item);
367 // MUST be called with cache lock held
368 void UpdateTimerPeriod (CacheItem item)
370 if (timedItems == null)
371 timedItems = new CacheItemPriorityQueue ();
373 long remaining = Math.Max (0, (long)(item.AbsoluteExpiration - DateTime.Now).TotalMilliseconds);
374 item.ExpiresAt = item.AbsoluteExpiration.Ticks;
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;
383 if (expirationTimer != null && expirationTimerPeriod <= remaining)
385 expirationTimerPeriod = remaining;
387 if (expirationTimer == null)
388 expirationTimer = new Timer (new TimerCallback (ExpireItems), null, expirationTimerPeriod, expirationTimerPeriod);
390 expirationTimer.Change (expirationTimerPeriod, expirationTimerPeriod);
393 // MUST be called with cache lock held
394 void EnqueueTimedItem (CacheItem item)
396 UpdateTimerPeriod (item);
397 timedItems.Enqueue (item);
400 public object Remove (string key)
402 return Remove (key, CacheItemRemovedReason.Removed, true, true);
405 internal object Remove (string key, CacheItemRemovedReason reason, bool doLock, bool invokeCallback)
410 cacheLock.EnterWriteLock ();
412 it = RemoveCacheItem (key);
415 // See comment at the top of the file, above cacheLock declaration
416 cacheLock.ExitWriteLock ();
422 if (it.Dependency != null) {
423 it.Dependency.SetCache (null);
424 it.Dependency.DependencyChanged -= new EventHandler (OnDependencyChanged);
425 it.Dependency.Dispose ();
427 if (invokeCallback && it.OnRemoveCallback != null) {
429 it.OnRemoveCallback (key, it.Value, reason);
431 //TODO: anything to be done here?
437 it.Dependency = null;
438 it.OnRemoveCallback = null;
439 it.OnUpdateCallback = null;
446 // Used when shutting down the application so that
447 // session_end events are sent for all sessions.
448 internal void InvokePrivateCallbacks ()
451 cacheLock.EnterReadLock ();
452 cache.InvokePrivateCallbacks ();
454 // See comment at the top of the file, above cacheLock declaration
455 cacheLock.ExitReadLock ();
459 public IDictionaryEnumerator GetEnumerator ()
461 List <CacheItem> list = null;
463 cacheLock.EnterReadLock ();
464 list = cache.ToList ();
466 // See comment at the top of the file, above cacheLock declaration
467 cacheLock.ExitReadLock ();
470 return new CacheItemEnumerator (list);
473 IEnumerator IEnumerable.GetEnumerator ()
475 return GetEnumerator ();
478 void OnDependencyChanged (object o, EventArgs a)
480 CheckDependencies ();
483 bool NeedsUpdate (CacheItem item, CacheItemUpdateReason reason, bool needLock)
487 cacheLock.EnterWriteLock ();
489 if (item == null || item.OnUpdateCallback == null)
492 object expensiveObject;
493 CacheDependency dependency;
494 DateTime absoluteExpiration;
495 TimeSpan slidingExpiration;
496 string key = item.Key;
497 CacheItemUpdateCallback updateCB = item.OnUpdateCallback;
499 updateCB (key, reason, out expensiveObject, out dependency, out absoluteExpiration, out slidingExpiration);
500 if (expensiveObject == null)
503 CacheItemPriority priority = item.Priority;
504 CacheItemRemovedCallback removeCB = item.OnRemoveCallback;
505 CacheItemRemovedReason whyRemoved;
508 case CacheItemUpdateReason.Expired:
509 whyRemoved = CacheItemRemovedReason.Expired;
512 case CacheItemUpdateReason.DependencyChanged:
513 whyRemoved = CacheItemRemovedReason.DependencyChanged;
517 whyRemoved = CacheItemRemovedReason.Removed;
521 Remove (key, whyRemoved, false, false);
522 Insert (key, expensiveObject, dependency, absoluteExpiration, slidingExpiration, priority, removeCB, updateCB, false);
525 } catch (Exception) {
529 // See comment at the top of the file, above cacheLock declaration
530 cacheLock.ExitWriteLock ();
535 void ExpireItems (object data)
537 DateTime now = DateTime.Now;
538 CacheItem item = null;
540 expirationTimer.Change (Timeout.Infinite, Timeout.Infinite);
542 cacheLock.EnterWriteLock ();
544 item = timedItems.Peek ();
547 if (timedItems.Count == 0)
550 timedItems.Dequeue ();
554 if (!item.Disabled && item.ExpiresAt > now.Ticks)
558 item = timedItems.Dequeue ();
562 item = timedItems.Dequeue ();
564 if (!NeedsUpdate (item, CacheItemUpdateReason.Expired, false))
565 Remove (item.Key, CacheItemRemovedReason.Expired, false, true);
568 // See comment at the top of the file, above cacheLock declaration
569 cacheLock.ExitWriteLock ();
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);
579 if (expirationTimerPeriod > 0)
583 expirationTimer.Change (Timeout.Infinite, Timeout.Infinite);
584 expirationTimerPeriod = 0;
587 internal void CheckDependencies ()
590 cacheLock.EnterWriteLock ();
591 List <CacheItem> list = cache.SelectItems (it => {
594 if (it.Dependency != null && it.Dependency.HasChanged && !NeedsUpdate (it, CacheItemUpdateReason.DependencyChanged, false))
599 foreach (CacheItem it in list)
600 Remove (it.Key, CacheItemRemovedReason.DependencyChanged, false, true);
604 // See comment at the top of the file, above cacheLock declaration
605 cacheLock.ExitWriteLock ();
609 internal DateTime GetKeyLastChange (string key)
612 cacheLock.EnterReadLock ();
613 CacheItem it = cache [key];
616 return DateTime.MaxValue;
618 return it.LastChange;
620 // See comment at the top of the file, above cacheLock declaration
621 cacheLock.ExitReadLock ();
625 internal Cache DependencyCache {
627 if (dependencyCache == null)
630 return dependencyCache;
632 set { dependencyCache = value; }