//------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. //------------------------------------------------------------ namespace System.Runtime.Collections { using System.Collections.Generic; // free-threaded so that it can deal with items releasing references and timer interactions // interaction pattern is: // 1) item = cache.Take(key); // 2) if (item == null) { Create and Open Item; cache.Add(key, value); } // 2) use item, including performing any blocking operations like open/close/etc // 3) item.ReleaseReference(); // // for usability purposes, if a CacheItem is non-null you can always call Release on it class ObjectCache where TValue : class { // for performance reasons we don't just blindly start a timer up to clean up // idle cache items. However, if we're above a certain threshold of items, then we'll start the timer. const int timerThreshold = 1; ObjectCacheSettings settings; Dictionary cacheItems; bool idleTimeoutEnabled; bool leaseTimeoutEnabled; IOThreadTimer idleTimer; static Action onIdle; bool disposed; public ObjectCache(ObjectCacheSettings settings) : this(settings, null) { } public ObjectCache(ObjectCacheSettings settings, IEqualityComparer comparer) { Fx.Assert(settings != null, "caller must use a valid settings object"); this.settings = settings.Clone(); this.cacheItems = new Dictionary(comparer); // idle feature is disabled if settings.IdleTimeout == TimeSpan.MaxValue this.idleTimeoutEnabled = (settings.IdleTimeout != TimeSpan.MaxValue); // lease feature is disabled if settings.LeaseTimeout == TimeSpan.MaxValue this.leaseTimeoutEnabled = (settings.LeaseTimeout != TimeSpan.MaxValue); } object ThisLock { get { return this; } } // Users like ServiceModel can hook this for ICommunicationObject or to handle other non-IDisposable objects public Action DisposeItemCallback { get; set; } public int Count { get { return this.cacheItems.Count; } } public ObjectCacheItem Add(TKey key, TValue value) { Fx.Assert(key != null, "caller must validate parameters"); Fx.Assert(value != null, "caller must validate parameters"); lock (ThisLock) { if (this.Count >= this.settings.CacheLimit || this.cacheItems.ContainsKey(key)) { // cache is full or already has an entry - return a shell CacheItem return new Item(key, value, this.DisposeItemCallback); } else { return InternalAdd(key, value); } } } public ObjectCacheItem Take(TKey key) { return Take(key, null); } // this overload is used for cases where a usable object can be atomically created in a non-blocking fashion public ObjectCacheItem Take(TKey key, Func initializerDelegate) { Fx.Assert(key != null, "caller must validate parameters"); Item cacheItem = null; lock (ThisLock) { if (this.cacheItems.TryGetValue(key, out cacheItem)) { // we have an item, add a reference cacheItem.InternalAddReference(); } else { if (initializerDelegate == null) { // not found in cache, no way to create. return null; } TValue createdObject = initializerDelegate(); Fx.Assert(createdObject != null, "initializer delegate must always give us a valid object"); if (this.Count >= this.settings.CacheLimit) { // cache is full - return a shell CacheItem return new Item(key, createdObject, this.DisposeItemCallback); } cacheItem = InternalAdd(key, createdObject); } } return cacheItem; } // assumes caller takes lock Item InternalAdd(TKey key, TValue value) { Item cacheItem = new Item(key, value, this); if (this.leaseTimeoutEnabled) { cacheItem.CreationTime = DateTime.UtcNow; } this.cacheItems.Add(key, cacheItem); StartTimerIfNecessary(); return cacheItem; } // assumes caller takes lock bool Return(TKey key, Item cacheItem) { bool disposeItem = false; if (this.disposed) { // we would have already disposed this item, do not attempt to return it disposeItem = true; } else { cacheItem.InternalReleaseReference(); DateTime now = DateTime.UtcNow; if (this.idleTimeoutEnabled) { cacheItem.LastUsage = now; } if (ShouldPurgeItem(cacheItem, now)) { bool removedFromItems = this.cacheItems.Remove(key); Fx.Assert(removedFromItems, "we should always find the key"); cacheItem.LockedDispose(); disposeItem = true; } } return disposeItem; } void StartTimerIfNecessary() { if (this.idleTimeoutEnabled && this.Count > timerThreshold) { if (this.idleTimer == null) { if (onIdle == null) { onIdle = new Action(OnIdle); } this.idleTimer = new IOThreadTimer(onIdle, this, false); } this.idleTimer.Set(this.settings.IdleTimeout); } } // timer callback static void OnIdle(object state) { ObjectCache cache = (ObjectCache)state; cache.PurgeCache(true); } static void Add(ref List list, T item) { Fx.Assert(!item.Equals(default(T)), "item should never be null"); if (list == null) { list = new List(); } list.Add(item); } bool ShouldPurgeItem(Item cacheItem, DateTime now) { // only prune items who aren't in use if (cacheItem.ReferenceCount > 0) { return false; } if (this.idleTimeoutEnabled && now >= (cacheItem.LastUsage + this.settings.IdleTimeout)) { return true; } else if (this.leaseTimeoutEnabled && (now - cacheItem.CreationTime) >= this.settings.LeaseTimeout) { return true; } return false; } void GatherExpiredItems(ref List> expiredItems, bool calledFromTimer) { if (this.Count == 0) { return; } if (!this.leaseTimeoutEnabled && !this.idleTimeoutEnabled) { return; } DateTime now = DateTime.UtcNow; bool setTimer = false; lock (ThisLock) { foreach (KeyValuePair cacheItem in this.cacheItems) { if (ShouldPurgeItem(cacheItem.Value, now)) { cacheItem.Value.LockedDispose(); Add(ref expiredItems, cacheItem); } } // now remove items from the cache if (expiredItems != null) { for (int i = 0; i < expiredItems.Count; i++) { this.cacheItems.Remove(expiredItems[i].Key); } } setTimer = calledFromTimer && (this.Count > 0); } if (setTimer) { idleTimer.Set(this.settings.IdleTimeout); } } void PurgeCache(bool calledFromTimer) { List> itemsToClose = null; lock (ThisLock) { this.GatherExpiredItems(ref itemsToClose, calledFromTimer); } if (itemsToClose != null) { for (int i = 0; i < itemsToClose.Count; i++) { itemsToClose[i].Value.LocalDispose(); } } } // dispose all the Items if they are IDisposable public void Dispose() { lock (ThisLock) { foreach (Item item in this.cacheItems.Values) { if (item != null) { // We need to Dispose every item in the cache even when it's refcount is greater than Zero, hence we call Dispose instead of LocalDispose item.Dispose(); } } this.cacheItems.Clear(); // we don't cache after Dispose this.settings.CacheLimit = 0; this.disposed = true; if (this.idleTimer != null) { this.idleTimer.Cancel(); this.idleTimer = null; } } } // public surface area is synchronized through this.parent.ThisLock class Item : ObjectCacheItem { readonly ObjectCache parent; readonly TKey key; readonly Action disposeItemCallback; TValue value; int referenceCount; public Item(TKey key, TValue value, Action disposeItemCallback) : this(key, value) { this.disposeItemCallback = disposeItemCallback; } public Item(TKey key, TValue value, ObjectCache parent) : this(key, value) { this.parent = parent; } Item(TKey key, TValue value) { this.key = key; this.value = value; this.referenceCount = 1; // start with a reference } public int ReferenceCount { get { return this.referenceCount; } } public override TValue Value { get { return this.value; } } public DateTime CreationTime { get; set; } public DateTime LastUsage { get; set; } public override bool TryAddReference() { bool result; // item may not be valid or cachable, first let's sniff for disposed without taking a lock if (this.parent == null || this.referenceCount == -1) { result = false; } else { bool disposeSelf = false; lock (this.parent.ThisLock) { if (this.referenceCount == -1) { result = false; } else if (this.referenceCount == 0 && this.parent.ShouldPurgeItem(this, DateTime.UtcNow)) { LockedDispose(); disposeSelf = true; result = false; this.parent.cacheItems.Remove(this.key); } else { // we're still in use, simply add-ref and be done this.referenceCount++; Fx.Assert(this.parent.cacheItems.ContainsValue(this), "should have a valid value"); Fx.Assert(this.Value != null, "should have a valid value"); result = true; } } if (disposeSelf) { this.LocalDispose(); } } return result; } public override void ReleaseReference() { bool disposeItem; if (this.parent == null) { Fx.Assert(this.referenceCount == 1, "reference count should have never increased"); this.referenceCount = -1; // not under a lock since we're not really in the cache disposeItem = true; } else { lock (this.parent.ThisLock) { // if our reference count will still be non zero, then simply decrement if (this.referenceCount > 1) { InternalReleaseReference(); disposeItem = false; } else { // otherwise we need to coordinate with our parent disposeItem = this.parent.Return(this.key, this); } } } if (disposeItem) { this.LocalDispose(); } } internal void InternalAddReference() { Fx.Assert(this.referenceCount >= 0, "cannot take the item marked for disposal"); this.referenceCount++; } internal void InternalReleaseReference() { Fx.Assert(this.referenceCount > 0, "can only release an item that has references"); this.referenceCount--; } // call this part under the lock, and Dispose outside the lock public void LockedDispose() { Fx.Assert(this.referenceCount == 0, "we should only dispose items without references"); this.referenceCount = -1; } public void Dispose() { if (Value != null) { Action localDisposeItemCallback = this.disposeItemCallback; if (this.parent != null) { Fx.Assert(localDisposeItemCallback == null, "shouldn't have both this.disposeItemCallback and this.parent"); localDisposeItemCallback = this.parent.DisposeItemCallback; } if (localDisposeItemCallback != null) { localDisposeItemCallback(Value); } else if (Value is IDisposable) { ((IDisposable)Value).Dispose(); } } this.value = null; // this will ensure that TryAddReference returns false this.referenceCount = -1; } public void LocalDispose() { Fx.Assert(this.referenceCount == -1, "we should only dispose items that have had LockedDispose called on them"); this.Dispose(); } } } }