Wed Feb 24 15:47:16 CET 2010 Paolo Molaro <lupus@ximian.com>
[mono.git] / mcs / class / System.Web / System.Web.Caching / Cache.cs
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.Security.Permissions;
35 using System.Web.Configuration;
36
37 namespace System.Web.Caching
38 {
39         // CAS - no InheritanceDemand here as the class is sealed
40         [AspNetHostingPermission (SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
41         public sealed class Cache: IEnumerable
42         {
43                 public static readonly DateTime NoAbsoluteExpiration = DateTime.MaxValue;
44                 public static readonly TimeSpan NoSlidingExpiration = TimeSpan.Zero;
45                 ReaderWriterLockSlim cacheLock;
46                 Dictionary <string, CacheItem> cache;
47                 CacheItemPriorityQueue timedItems;
48                 Timer expirationTimer;
49                 long expirationTimerPeriod = 0;
50                 Cache dependencyCache;
51                 bool? disableExpiration;
52                 long privateBytesLimit = -1;
53                 long percentagePhysicalMemoryLimit = -1;
54                 
55                 bool DisableExpiration {
56                         get {
57                                 if (disableExpiration == null) {
58                                         var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection;
59                                         if (cs == null)
60                                                 disableExpiration = false;
61                                         else
62                                                 disableExpiration = (bool)cs.DisableExpiration;
63                                 }
64
65                                 return (bool)disableExpiration;
66                         }
67                 }
68
69                 public long EffectivePrivateBytesLimit {
70                         get {
71                                 if (privateBytesLimit == -1) {
72                                         var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection;
73                                         if (cs == null)
74                                                 privateBytesLimit = 0;
75                                         else
76                                                 privateBytesLimit = cs.PrivateBytesLimit;
77
78                                         if (privateBytesLimit == 0) {
79                                                 // http://blogs.msdn.com/tmarq/archive/2007/06/25/some-history-on-the-asp-net-cache-memory-limits.aspx
80                                                 // TODO: calculate
81                                                 privateBytesLimit = 734003200;
82                                         }
83                                 }
84
85                                 return privateBytesLimit;
86                         }
87                 }
88
89                 public long EffectivePercentagePhysicalMemoryLimit {
90                         get {
91                                 if (percentagePhysicalMemoryLimit == -1) {
92                                         var cs = WebConfigurationManager.GetWebApplicationSection ("system.web/caching/cache") as CacheSection;
93                                         if (cs == null)
94                                                 percentagePhysicalMemoryLimit = 0;
95                                         else
96                                                 percentagePhysicalMemoryLimit = cs.PercentagePhysicalMemoryUsedLimit;
97
98                                         if (percentagePhysicalMemoryLimit == 0) {
99                                                 // http://blogs.msdn.com/tmarq/archive/2007/06/25/some-history-on-the-asp-net-cache-memory-limits.aspx
100                                                 // TODO: calculate
101                                                 percentagePhysicalMemoryLimit = 97;
102                                         }
103                                 }
104
105                                 return percentagePhysicalMemoryLimit;
106                         }
107                 }
108                 
109                 public Cache ()
110                 {
111                         cacheLock = new ReaderWriterLockSlim ();
112                         cache = new Dictionary <string, CacheItem> (StringComparer.Ordinal);
113                 }
114
115                 public int Count {
116                         get { return cache.Count; }
117                 }
118                 
119                 public object this [string key] {
120                         get { return Get (key); }
121                         set { Insert (key, value); }
122                 }
123
124                 CacheItem GetCacheItem (string key)
125                 {
126                         if (key == null)
127                                 return null;
128                         
129                         CacheItem ret;
130                         if (cache.TryGetValue (key, out ret))
131                                 return ret;
132                         return null;
133                 }
134
135                 CacheItem RemoveCacheItem (string key)
136                 {
137                         if (key == null)
138                                 return null;
139
140                         CacheItem ret = null;
141                         if (!cache.TryGetValue (key, out ret))
142                                 return null;
143                         if (timedItems != null)
144                                 timedItems.OnItemDisable (ret);
145                         
146                         ret.Disabled = true;
147                         cache.Remove (key);
148                         
149                         return ret;
150                 }
151                 
152                 public object Add (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)
153                 {
154                         if (key == null)
155                                 throw new ArgumentNullException ("key");
156
157                         bool locked = false;
158                         try {
159                                 cacheLock.EnterWriteLock ();
160                                 locked = true;
161                                 CacheItem it = GetCacheItem (key);
162
163                                 if (it != null)
164                                         return it.Value;
165                                 Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback, null, false);
166                         } finally {
167                                 if (locked)
168                                         cacheLock.ExitWriteLock ();
169                         }
170                                 
171                         return null;
172                 }
173                 
174                 public object Get (string key)
175                 {
176                         bool locked = false;
177                         try {
178                                 cacheLock.EnterUpgradeableReadLock ();
179                                 locked = true;
180                                 CacheItem it = GetCacheItem (key);
181                                 if (it == null)
182                                         return null;
183                                 
184                                 if (it.Dependency != null && it.Dependency.HasChanged) {
185                                         try {
186                                                 cacheLock.EnterWriteLock ();
187                                                 if (!NeedsUpdate (it, CacheItemUpdateReason.DependencyChanged, false))
188                                                         Remove (it.Key, CacheItemRemovedReason.DependencyChanged, false, true);
189                                         } finally {
190                                                 cacheLock.ExitWriteLock ();
191                                         }
192                                         
193                                         return null;
194                                 }
195
196                                 if (!DisableExpiration) {
197                                         if (it.SlidingExpiration != NoSlidingExpiration) {
198                                                 it.AbsoluteExpiration = DateTime.Now + it.SlidingExpiration;
199                                                 // Cast to long is ok since we know that sliding expiration
200                                                 // is less than 365 days (31536000000ms)
201                                                 long remaining = (long)it.SlidingExpiration.TotalMilliseconds;
202                                                 it.ExpiresAt = it.AbsoluteExpiration.Ticks;
203                                                 
204                                                 if (expirationTimer != null && (expirationTimerPeriod == 0 || expirationTimerPeriod > remaining)) {
205                                                         expirationTimerPeriod = remaining;
206                                                         expirationTimer.Change (expirationTimerPeriod, expirationTimerPeriod);
207                                                 }
208                                         
209                                         } else if (DateTime.Now >= it.AbsoluteExpiration) {
210                                                 try {
211                                                         cacheLock.EnterWriteLock ();
212                                                         if (!NeedsUpdate (it, CacheItemUpdateReason.Expired, false))
213                                                                 Remove (key, CacheItemRemovedReason.Expired, false, true);
214                                                 } finally {
215                                                         cacheLock.ExitWriteLock ();
216                                                 }
217
218                                                 return null;
219                                         }
220                                 }
221                                 
222                                 return it.Value;
223                         } finally {
224                                 if (locked) {
225                                         cacheLock.ExitUpgradeableReadLock ();
226                                 }
227                         }
228                 }
229                 
230                 public void Insert (string key, object value)
231                 {
232                         Insert (key, value, null, NoAbsoluteExpiration, NoSlidingExpiration, CacheItemPriority.Normal, null, null, true);
233                 }
234                 
235                 public void Insert (string key, object value, CacheDependency dependencies)
236                 {
237                         Insert (key, value, dependencies, NoAbsoluteExpiration, NoSlidingExpiration, CacheItemPriority.Normal, null, null, true);
238                 }
239                 
240                 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration)
241                 {
242                         Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, CacheItemPriority.Normal, null, null, true);
243                 }
244
245                 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration,
246                                     CacheItemUpdateCallback onUpdateCallback)
247                 {
248                         Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, CacheItemPriority.Normal, null, onUpdateCallback, true);
249                 }
250                 
251                 public void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration,
252                                     CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)
253                 {
254                         Insert (key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback, null, true);
255                 }
256
257                 void Insert (string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration,
258                              CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback, CacheItemUpdateCallback onUpdateCallback, bool doLock)
259                 {
260                         if (key == null)
261                                 throw new ArgumentNullException ("key");
262                         if (value == null)
263                                 throw new ArgumentNullException ("value");
264                         if (slidingExpiration < TimeSpan.Zero || slidingExpiration > TimeSpan.FromDays (365))
265                                 throw new ArgumentNullException ("slidingExpiration");
266                         if (absoluteExpiration != NoAbsoluteExpiration && slidingExpiration != NoSlidingExpiration)
267                                 throw new ArgumentException ("Both absoluteExpiration and slidingExpiration are specified");
268                                 
269                         CacheItem ci = new CacheItem ();
270                         ci.Value = value;
271                         ci.Key = key;
272                         
273                         if (dependencies != null) {
274                                 ci.Dependency = dependencies;
275                                 dependencies.DependencyChanged += new EventHandler (OnDependencyChanged);
276                                 dependencies.SetCache (DependencyCache);
277                         }
278
279                         ci.Priority = priority;
280                         SetItemTimeout (ci, absoluteExpiration, slidingExpiration, onRemoveCallback, onUpdateCallback, key, doLock);
281                 }
282                 
283                 internal void SetItemTimeout (string key, DateTime absoluteExpiration, TimeSpan slidingExpiration, bool doLock)
284                 {
285                         CacheItem ci = null;
286                         bool locked = false;
287                         
288                         try {
289                                 if (doLock) {
290                                         cacheLock.EnterWriteLock ();
291                                         locked = true;
292                                 }
293                                 
294                                 ci = GetCacheItem (key);
295                                 if (ci != null)
296                                         SetItemTimeout (ci, absoluteExpiration, slidingExpiration, ci.OnRemoveCallback, null, key, false);
297                         } finally {
298                                 if (locked) {
299                                         cacheLock.ExitWriteLock ();
300                                 }
301                         }
302                 }
303
304                 void SetItemTimeout (CacheItem ci, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemRemovedCallback onRemoveCallback,
305                                      CacheItemUpdateCallback onUpdateCallback, string key, bool doLock)
306                 {
307                         bool disableExpiration = DisableExpiration;
308
309                         if (!disableExpiration) {
310                                 ci.SlidingExpiration = slidingExpiration;
311                                 if (slidingExpiration != NoSlidingExpiration)
312                                         ci.AbsoluteExpiration = DateTime.Now + slidingExpiration;
313                                 else
314                                         ci.AbsoluteExpiration = absoluteExpiration;                     
315                         }
316                         
317                         ci.OnRemoveCallback = onRemoveCallback;
318                         ci.OnUpdateCallback = onUpdateCallback;
319                         
320                         bool locked = false;
321                         try {
322                                 if (doLock) {
323                                         cacheLock.EnterWriteLock ();
324                                         locked = true;
325                                 }
326                                 
327                                 if (ci.Timer != null) {
328                                         ci.Timer.Dispose ();
329                                         ci.Timer = null;
330                                 }
331
332                                 if (key != null)
333                                         cache [key] = ci;
334                                 
335                                 ci.LastChange = DateTime.Now;
336                                 if (!disableExpiration && ci.AbsoluteExpiration != NoAbsoluteExpiration)
337                                         EnqueueTimedItem (ci);
338                         } finally {
339                                 if (locked) {
340                                         cacheLock.ExitWriteLock ();
341                                 }
342                         }
343                 }
344
345                 // MUST be called with cache lock held
346                 void EnqueueTimedItem (CacheItem item)
347                 {
348                         long remaining = Math.Max (0, (long)(item.AbsoluteExpiration - DateTime.Now).TotalMilliseconds);
349                         item.ExpiresAt = item.AbsoluteExpiration.Ticks;
350                         
351                         if (timedItems == null)
352                                 timedItems = new CacheItemPriorityQueue ();
353                         
354                         if (remaining > 4294967294)
355                                 // Maximum due time for timer
356                                 // Item will expire properly anyway, as the timer will be
357                                 // rescheduled for the item's expiration time once that item is
358                                 // bubbled to the top of the priority queue.
359                                 expirationTimerPeriod = 4294967294;
360                         else
361                                 expirationTimerPeriod = remaining;
362
363                         if (expirationTimer == null)
364                                 expirationTimer = new Timer (new TimerCallback (ExpireItems), null, expirationTimerPeriod, expirationTimerPeriod);
365                         else
366                                 expirationTimer.Change (expirationTimerPeriod, expirationTimerPeriod);
367                         
368                         timedItems.Enqueue (item);
369                 }
370
371                 public object Remove (string key)
372                 {
373                         return Remove (key, CacheItemRemovedReason.Removed, true, true);
374                 }
375                 
376                 object Remove (string key, CacheItemRemovedReason reason, bool doLock, bool invokeCallback)
377                 {
378                         CacheItem it = null;
379                         bool locked = false;
380                         try {
381                                 if (doLock) {
382                                         cacheLock.EnterWriteLock ();
383                                         locked = true;
384                                 }
385                                 
386                                 it = RemoveCacheItem (key);
387                         } finally {
388                                 if (locked) {
389                                         cacheLock.ExitWriteLock ();
390                                 }
391                         }
392
393                         if (it != null) {
394                                 Timer t = it.Timer;
395                                 if (t != null)
396                                         t.Dispose ();
397                                 
398                                 if (it.Dependency != null) {
399                                         it.Dependency.SetCache (null);
400                                         it.Dependency.DependencyChanged -= new EventHandler (OnDependencyChanged);
401                                         it.Dependency.Dispose ();
402                                 }
403                                 if (invokeCallback && it.OnRemoveCallback != null) {
404                                         try {
405                                                 it.OnRemoveCallback (key, it.Value, reason);
406                                         } catch {
407                                                 //TODO: anything to be done here?
408                                         }
409                                 }
410                                 object ret = it.Value;
411                                 it.Value = null;
412                                 it.Key = null;
413                                 it.Dependency = null;
414                                 it.OnRemoveCallback = null;
415                                 it.OnUpdateCallback = null;
416
417                                 return ret;
418                         } else
419                                 return null;
420                 }
421
422                 // Used when shutting down the application so that
423                 // session_end events are sent for all sessions.
424                 internal void InvokePrivateCallbacks ()
425                 {
426                         CacheItemRemovedReason reason = CacheItemRemovedReason.Removed;
427                         bool locked = false;
428                         try {
429                                 cacheLock.EnterReadLock ();
430                                 locked = true;
431                                 foreach (string key in cache.Keys) {
432                                         CacheItem item = GetCacheItem (key);
433                                         if (item.Disabled)
434                                                 continue;
435                                         
436                                         if (item != null && item.OnRemoveCallback != null) {
437                                                 try {
438                                                         item.OnRemoveCallback (key, item.Value, reason);
439                                                 } catch {
440                                                         //TODO: anything to be done here?
441                                                 }
442                                         }
443                                 }
444                         }  finally {
445                                 if (locked) {
446                                         cacheLock.ExitReadLock ();
447                                 }
448                         }
449                 }
450
451                 public IDictionaryEnumerator GetEnumerator ()
452                 {
453                         ArrayList list = new ArrayList ();
454                         bool locked = false;
455                         try {
456                                 cacheLock.EnterReadLock ();
457                                 locked = true;
458                                 foreach (CacheItem it in cache.Values)
459                                         list.Add (it);
460                         } finally {
461                                 if (locked) {
462                                         cacheLock.ExitReadLock ();
463                                 }
464                         }
465                         
466                         return new CacheItemEnumerator (list);
467                 }
468                 
469                 IEnumerator IEnumerable.GetEnumerator ()
470                 {
471                         return GetEnumerator ();
472                 }
473                 
474                 void OnDependencyChanged (object o, EventArgs a)
475                 {
476                         CheckDependencies ();
477                 }
478
479                 bool NeedsUpdate (CacheItem item, CacheItemUpdateReason reason, bool needLock)
480                 {
481                         bool locked = false;
482                         
483                         try {
484                                 if (needLock) {
485                                         cacheLock.EnterWriteLock ();
486                                         locked = true;
487                                 }
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 (locked)
529                                         cacheLock.ExitWriteLock ();
530                         }
531                 }
532                 
533                 void ExpireItems (object data)
534                 {
535                         DateTime now = DateTime.Now;
536                         CacheItem item = timedItems.Peek ();
537                         bool locked = false;
538
539                         try {
540                                 cacheLock.EnterWriteLock ();
541                                 locked = true;
542
543                                 while (item != null) {
544                                         if (!item.Disabled && item.ExpiresAt > now.Ticks)
545                                                 break;
546                                         if (item.Disabled) {
547                                                 item = timedItems.Dequeue ();
548                                                 continue;
549                                         }
550
551                                         item = timedItems.Dequeue ();
552                                         if (!NeedsUpdate (item, CacheItemUpdateReason.Expired, true))
553                                                 Remove (item.Key, CacheItemRemovedReason.Expired, false, true);
554                                         item = timedItems.Peek ();
555                                 }
556                         } finally {
557                                 if (locked)
558                                         cacheLock.ExitWriteLock ();
559                         }
560
561                         if (item != null) {
562                                 long remaining = Math.Max (0, (long)(item.AbsoluteExpiration - now).TotalMilliseconds);
563                                 if (expirationTimerPeriod != remaining && remaining > 0) {
564                                         expirationTimerPeriod = remaining;
565                                         expirationTimer.Change (expirationTimerPeriod, expirationTimerPeriod);
566                                 }
567                                 return;
568                         }
569
570                         expirationTimer.Change (Timeout.Infinite, Timeout.Infinite);
571                         expirationTimerPeriod = 0;
572                 }
573                 
574                 internal void CheckDependencies ()
575                 {
576                         IList list;
577                         bool locked = false;
578                         try {
579                                 cacheLock.EnterWriteLock ();
580                                 locked = true;
581                                 list = new List <CacheItem> ();
582                                 foreach (CacheItem it in cache.Values)
583                                         list.Add (it);
584                         
585                                 foreach (CacheItem it in list) {
586                                         if (it.Dependency != null && it.Dependency.HasChanged && !NeedsUpdate (it, CacheItemUpdateReason.DependencyChanged, false))
587                                                 Remove (it.Key, CacheItemRemovedReason.DependencyChanged, false, true);
588                                 }
589                         } finally {
590                                 if (locked) {
591                                         cacheLock.ExitWriteLock ();
592                                 }
593                         }
594                 }
595                 
596                 internal DateTime GetKeyLastChange (string key)
597                 {
598                         bool locked = false;
599                         try {
600                                 cacheLock.EnterReadLock ();
601                                 locked = true;
602                                 CacheItem it = GetCacheItem (key);
603
604                                 if (it == null)
605                                         return DateTime.MaxValue;
606                                 
607                                 return it.LastChange;
608                         } finally {
609                                 if (locked) {
610                                         cacheLock.ExitReadLock ();
611                                 }
612                         }
613                 }
614
615                 internal Cache DependencyCache {
616                         get {
617                                 if (dependencyCache == null)
618                                         return this;
619
620                                 return dependencyCache;
621                         }
622                         set { dependencyCache = value; }
623                 }
624         }
625 }