using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Caching;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web.Caching;
using Umbraco.Core.Logging;
using CacheItemPriority = System.Web.Caching.CacheItemPriority;
namespace Umbraco.Core.Cache
{
///
/// A cache provider that wraps the logic of a System.Runtime.Caching.ObjectCache
///
internal class ObjectCacheRuntimeCacheProvider : IRuntimeCacheProvider
{
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
internal ObjectCache MemoryCache;
// an object that represent a value that has not been created yet
protected readonly object ValueNotCreated = new object();
///
/// Used for debugging
///
internal Guid InstanceId { get; private set; }
public ObjectCacheRuntimeCacheProvider()
{
MemoryCache = new MemoryCache("in-memory");
InstanceId = Guid.NewGuid();
}
protected object GetSafeLazyValue(Lazy lazy, bool onlyIfValueIsCreated = false)
{
try
{
// if onlyIfValueIsCreated, do not trigger value creation
// must return something, though, to differenciate from null values
if (onlyIfValueIsCreated && lazy.IsValueCreated == false) return ValueNotCreated;
return lazy.Value;
}
catch
{
return null;
}
}
#region Clear
public virtual void ClearAllCache()
{
using (new WriteLock(_locker))
{
MemoryCache.DisposeIfDisposable();
MemoryCache = new MemoryCache("in-memory");
}
}
public virtual void ClearCacheItem(string key)
{
using (new WriteLock(_locker))
{
if (MemoryCache[key] == null) return;
MemoryCache.Remove(key);
}
}
public virtual void ClearCacheObjectTypes(string typeName)
{
using (new WriteLock(_locker))
{
foreach (var key in MemoryCache
.Where(x =>
{
// x.Value is Lazy and not null, its value may be null
// remove null values as well, does not hurt
// get non-created as NonCreatedValue & exceptions as null
var value = GetSafeLazyValue((Lazy)x.Value, true);
return value == null || value.GetType().ToString().InvariantEquals(typeName);
})
.Select(x => x.Key)
.ToArray()) // ToArray required to remove
MemoryCache.Remove(key);
}
}
public virtual void ClearCacheObjectTypes()
{
using (new WriteLock(_locker))
{
var typeOfT = typeof (T);
foreach (var key in MemoryCache
.Where(x =>
{
// x.Value is Lazy and not null, its value may be null
// remove null values as well, does not hurt
// get non-created as NonCreatedValue & exceptions as null
var value = GetSafeLazyValue((Lazy)x.Value, true);
//TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329
// until then we will check if 'T' is an interface and if so we will use the 'is' clause,
// otherwise we do an exact match.
return value == null ||
(typeOfT.IsInterface
? (value is T)
: value.GetType() == typeOfT);
})
.Select(x => x.Key)
.ToArray()) // ToArray required to remove
MemoryCache.Remove(key);
}
}
public virtual void ClearCacheObjectTypes(Func predicate)
{
using (new WriteLock(_locker))
{
var typeOfT = typeof(T);
foreach (var key in MemoryCache
.Where(x =>
{
// x.Value is Lazy and not null, its value may be null
// remove null values as well, does not hurt
// get non-created as NonCreatedValue & exceptions as null
var value = GetSafeLazyValue((Lazy)x.Value, true);
if (value == null) return true;
//TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329
// until then we will check if 'T' is an interface and if so we will use the 'is' clause,
// otherwise we do an exact match.
return ((typeOfT.IsInterface && value is T) || (value.GetType() == typeOfT))
&& predicate(x.Key, (T)value);
})
.Select(x => x.Key)
.ToArray()) // ToArray required to remove
MemoryCache.Remove(key);
}
}
public virtual void ClearCacheByKeySearch(string keyStartsWith)
{
using (new WriteLock(_locker))
{
foreach (var key in MemoryCache
.Where(x => x.Key.InvariantStartsWith(keyStartsWith))
.Select(x => x.Key)
.ToArray()) // ToArray required to remove
MemoryCache.Remove(key);
}
}
public virtual void ClearCacheByKeyExpression(string regexString)
{
using (new WriteLock(_locker))
{
foreach (var key in MemoryCache
.Where(x => Regex.IsMatch(x.Key, regexString))
.Select(x => x.Key)
.ToArray()) // ToArray required to remove
MemoryCache.Remove(key);
}
}
#endregion
#region Get
public IEnumerable GetCacheItemsByKeySearch(string keyStartsWith)
{
KeyValuePair[] entries;
using (new ReadLock(_locker))
{
entries = MemoryCache
.Where(x => x.Key.InvariantStartsWith(keyStartsWith))
.ToArray(); // evaluate while locked
}
return entries
.Select(x => GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null
.Where(x => x != null) // backward compat, don't store null values in the cache
.ToList();
}
public IEnumerable GetCacheItemsByKeyExpression(string regexString)
{
KeyValuePair[] entries;
using (new ReadLock(_locker))
{
entries = MemoryCache
.Where(x => Regex.IsMatch(x.Key, regexString))
.ToArray(); // evaluate while locked
}
return entries
.Select(x => GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null
.Where(x => x != null) // backward compat, don't store null values in the cache
.ToList();
}
public object GetCacheItem(string cacheKey)
{
Lazy result;
using (new ReadLock(_locker))
{
result = MemoryCache.Get(cacheKey) as Lazy; // null if key not found
}
return result == null ? null : GetSafeLazyValue(result); // return exceptions as null
}
public object GetCacheItem(string cacheKey, Func getCacheItem)
{
return GetCacheItem(cacheKey, getCacheItem, null);
}
public object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal,CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null)
{
// see notes in HttpRuntimeCacheProvider
Lazy result;
using (var lck = new UpgradeableReadLock(_locker))
{
result = MemoryCache.Get(cacheKey) as Lazy;
if (result == null || GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null
{
result = new Lazy(getCacheItem);
var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles);
lck.UpgradeToWriteLock();
MemoryCache.Set(cacheKey, result, policy);
}
}
return result.Value;
}
#endregion
#region Insert
public void InsertCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null)
{
// NOTE - here also we must insert a Lazy but we can evaluate it right now
// and make sure we don't store a null value.
var result = new Lazy(getCacheItem);
var value = result.Value; // force evaluation now
if (value == null) return; // do not store null values (backward compat)
var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles);
MemoryCache.Set(cacheKey, result, policy);
}
#endregion
private static CacheItemPolicy GetPolicy(TimeSpan? timeout = null, bool isSliding = false, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null)
{
var absolute = isSliding ? ObjectCache.InfiniteAbsoluteExpiration : (timeout == null ? ObjectCache.InfiniteAbsoluteExpiration : DateTime.Now.Add(timeout.Value));
var sliding = isSliding == false ? ObjectCache.NoSlidingExpiration : (timeout ?? ObjectCache.NoSlidingExpiration);
var policy = new CacheItemPolicy
{
AbsoluteExpiration = absolute,
SlidingExpiration = sliding
};
if (dependentFiles != null && dependentFiles.Any())
{
policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList()));
}
if (removedCallback != null)
{
policy.RemovedCallback = arguments =>
{
//convert the reason
var reason = CacheItemRemovedReason.Removed;
switch (arguments.RemovedReason)
{
case CacheEntryRemovedReason.Removed:
reason = CacheItemRemovedReason.Removed;
break;
case CacheEntryRemovedReason.Expired:
reason = CacheItemRemovedReason.Expired;
break;
case CacheEntryRemovedReason.Evicted:
reason = CacheItemRemovedReason.Underused;
break;
case CacheEntryRemovedReason.ChangeMonitorChanged:
reason = CacheItemRemovedReason.Expired;
break;
case CacheEntryRemovedReason.CacheSpecificEviction:
reason = CacheItemRemovedReason.Underused;
break;
}
//call the callback
removedCallback(arguments.CacheItem.Key, arguments.CacheItem.Value, reason);
};
}
return policy;
}
}
}