e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
509 lines
16 KiB
C#
509 lines
16 KiB
C#
//------------------------------------------------------------
|
|
// 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<TKey, TValue>
|
|
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<TKey, Item> cacheItems;
|
|
bool idleTimeoutEnabled;
|
|
bool leaseTimeoutEnabled;
|
|
IOThreadTimer idleTimer;
|
|
static Action<object> onIdle;
|
|
bool disposed;
|
|
|
|
public ObjectCache(ObjectCacheSettings settings)
|
|
: this(settings, null)
|
|
{
|
|
}
|
|
|
|
public ObjectCache(ObjectCacheSettings settings, IEqualityComparer<TKey> comparer)
|
|
{
|
|
Fx.Assert(settings != null, "caller must use a valid settings object");
|
|
this.settings = settings.Clone();
|
|
this.cacheItems = new Dictionary<TKey, Item>(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<TValue> DisposeItemCallback
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public int Count
|
|
{
|
|
get
|
|
{
|
|
return this.cacheItems.Count;
|
|
}
|
|
}
|
|
|
|
public ObjectCacheItem<TValue> 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<TValue> 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<TValue> Take(TKey key, Func<TValue> 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<object>(OnIdle);
|
|
}
|
|
|
|
this.idleTimer = new IOThreadTimer(onIdle, this, false);
|
|
}
|
|
|
|
this.idleTimer.Set(this.settings.IdleTimeout);
|
|
}
|
|
}
|
|
|
|
// timer callback
|
|
static void OnIdle(object state)
|
|
{
|
|
ObjectCache<TKey, TValue> cache = (ObjectCache<TKey, TValue>)state;
|
|
cache.PurgeCache(true);
|
|
}
|
|
|
|
static void Add<T>(ref List<T> list, T item)
|
|
{
|
|
Fx.Assert(!item.Equals(default(T)), "item should never be null");
|
|
if (list == null)
|
|
{
|
|
list = new List<T>();
|
|
}
|
|
|
|
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<KeyValuePair<TKey, Item>> 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<TKey, Item> 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<KeyValuePair<TKey, Item>> 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<TValue>
|
|
{
|
|
readonly ObjectCache<TKey, TValue> parent;
|
|
readonly TKey key;
|
|
readonly Action<TValue> disposeItemCallback;
|
|
|
|
TValue value;
|
|
int referenceCount;
|
|
|
|
public Item(TKey key, TValue value, Action<TValue> disposeItemCallback)
|
|
: this(key, value)
|
|
{
|
|
this.disposeItemCallback = disposeItemCallback;
|
|
}
|
|
|
|
public Item(TKey key, TValue value, ObjectCache<TKey, TValue> 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<TValue> 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();
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|