using System;
using System.Collections.Specialized;
using System.Globalization;
using System.Runtime.Caching;
using CacheManager.Core;
using CacheManager.Core.Internal;
using CacheManager.Core.Logging;
using CacheManager.Core.Utility;
namespace PhoenixLib.Caching
{
///
/// Simple implementation for the .
///
/// The type of the cache value.
///
/// Although the MemoryCache doesn't support regions nor a RemoveAll/Clear method, we will
/// implement it via cache dependencies.
///
public class MemoryCacheHandle : BaseCacheHandle
{
private const string DefaultName = "default";
// can be default or any other name
private readonly string _cacheName = string.Empty;
private volatile MemoryCache _cache;
private string _instanceKey;
private int _instanceKeyLength;
///
/// Initializes a new instance of the class.
///
/// The manager configuration.
/// The cache handle configuration.
/// The logger factory.
public MemoryCacheHandle(ICacheManagerConfiguration managerConfiguration, CacheHandleConfiguration configuration, ILoggerFactory loggerFactory)
: base(managerConfiguration, configuration)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(loggerFactory, nameof(loggerFactory));
Logger = loggerFactory.CreateLogger(this);
_cacheName = configuration.Name;
_cache = _cacheName.ToUpper(CultureInfo.InvariantCulture).Equals(DefaultName.ToUpper(CultureInfo.InvariantCulture))
? MemoryCache.Default
: new MemoryCache(_cacheName);
_instanceKey = Guid.NewGuid().ToString();
_instanceKeyLength = _instanceKey.Length;
CreateInstanceToken();
}
///
/// Gets the cache settings.
///
/// The cache settings.
public NameValueCollection CacheSettings => GetSettings(_cache);
///
/// Gets the number of items the cache handle currently maintains.
///
/// The count.
public override int Count => (int)_cache.GetCount();
///
protected override ILogger Logger { get; }
///
/// Clears this cache, removing all items in the base cache and all regions.
///
public override void Clear()
{
_cache.Remove(_instanceKey);
CreateInstanceToken();
}
///
/// Clears the cache region, removing all items from the specified only.
///
/// The cache region.
public override void ClearRegion(string region) =>
_cache.Remove(GetRegionTokenKey(region));
///
public override bool Exists(string key) => _cache.Contains(GetItemKey(key));
///
public override bool Exists(string key, string region)
{
Guard.NotNullOrWhiteSpace(region, nameof(region));
string fullKey = GetItemKey(key, region);
return _cache.Contains(fullKey);
}
///
/// Adds a value to the cache.
///
/// The CacheItem to be added to the cache.
///
/// true if the key was not already added to the cache, false otherwise.
///
protected override bool AddInternalPrepared(CacheItem item)
{
string key = GetItemKey(item);
if (_cache.Contains(key))
{
return false;
}
CacheItemPolicy policy = GetPolicy(item);
return _cache.Add(key, item, policy);
}
///
/// Gets a CacheItem for the specified key.
///
/// The key being used to identify the item within the cache.
/// The CacheItem.
protected override CacheItem GetCacheItemInternal(string key) => GetCacheItemInternal(key, null);
///
/// Gets a CacheItem for the specified key.
///
/// The key being used to identify the item within the cache.
/// The cache region.
/// The CacheItem.
protected override CacheItem GetCacheItemInternal(string key, string region)
{
string fullKey = GetItemKey(key, region);
if (!(_cache.Get(fullKey) is CacheItem item))
{
return null;
}
// maybe the item is already expired because MemoryCache implements a default interval
// of 20 seconds! to check for expired items on each store, we do it on access to also
// reflect smaller time frames especially for sliding expiration...
// cache.Get eventually triggers eviction callback, but just in case...
if (item.IsExpired)
{
RemoveInternal(item.Key, item.Region);
TriggerCacheSpecificRemove(item.Key, item.Region, CacheItemRemovedReason.Expired, item.Value);
return null;
}
if (item.ExpirationMode == ExpirationMode.Sliding)
{
// because we don't use UpdateCallback because of some multithreading issues lets
// try to simply reset the item by setting it again.
// item = this.GetItemExpiration(item); // done via base cache handle
_cache.Set(fullKey, item, GetPolicy(item));
}
return item;
}
///
/// Puts the into the cache. If the item exists it will get updated
/// with the new value. If the item doesn't exist, the item will be added to the cache.
///
/// The CacheItem to be added to the cache.
protected override void PutInternalPrepared(CacheItem item)
{
string key = GetItemKey(item);
CacheItemPolicy policy = GetPolicy(item);
_cache.Set(key, item, policy);
}
///
/// Removes a value from the cache for the specified key.
///
/// The key being used to identify the item within the cache.
///
/// true if the key was found and removed from the cache, false otherwise.
///
protected override bool RemoveInternal(string key) => RemoveInternal(key, null);
///
/// Removes a value from the cache for the specified key.
///
/// The key being used to identify the item within the cache.
/// The cache region.
///
/// true if the key was found and removed from the cache, false otherwise.
///
protected override bool RemoveInternal(string key, string region)
{
string fullKey = GetItemKey(key, region);
object obj = _cache.Remove(fullKey);
return obj != null;
}
private static NameValueCollection GetSettings(MemoryCache instance)
{
var cacheCfg = new NameValueCollection
{
{ "CacheMemoryLimitMegabytes", (instance.CacheMemoryLimit / 1024 / 1024).ToString(CultureInfo.InvariantCulture) },
{ "PhysicalMemoryLimitPercentage", instance.PhysicalMemoryLimit.ToString(CultureInfo.InvariantCulture) },
{ "PollingInterval", instance.PollingInterval.ToString() }
};
return cacheCfg;
}
private void CreateInstanceToken()
{
// don't add a new key while we are disposing our instance
if (Disposing)
{
return;
}
var instanceItem = new CacheItem(_instanceKey, _instanceKey);
var policy = new CacheItemPolicy
{
Priority = CacheItemPriority.NotRemovable,
RemovedCallback = InstanceTokenRemoved,
AbsoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration,
SlidingExpiration = ObjectCache.NoSlidingExpiration
};
_cache.Add(instanceItem.Key, instanceItem, policy);
}
private void CreateRegionToken(string region)
{
string key = GetRegionTokenKey(region);
// add region token with dependency on our instance token, so that all regions get
// removed whenever the instance gets cleared.
var policy = new CacheItemPolicy
{
Priority = CacheItemPriority.NotRemovable,
AbsoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration,
SlidingExpiration = ObjectCache.NoSlidingExpiration,
ChangeMonitors = { _cache.CreateCacheEntryChangeMonitor(new[] { _instanceKey }) }
};
_cache.Add(key, region, policy);
}
private CacheItemPolicy GetPolicy(CacheItem item)
{
string[] monitorKeys = { _instanceKey };
if (!string.IsNullOrWhiteSpace(item.Region))
{
// this should be the only place to create the region token if it doesn't exist it
// might got removed by clearRegion but next time put or add gets called, the region
// should be re added...
string regionToken = GetRegionTokenKey(item.Region);
if (!_cache.Contains(regionToken))
{
CreateRegionToken(item.Region);
}
monitorKeys = new[] { _instanceKey, regionToken };
}
var policy = new CacheItemPolicy
{
Priority = CacheItemPriority.Default,
ChangeMonitors = { _cache.CreateCacheEntryChangeMonitor(monitorKeys) },
AbsoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration,
SlidingExpiration = ObjectCache.NoSlidingExpiration
};
switch (item.ExpirationMode)
{
case ExpirationMode.Absolute:
policy.AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.Add(item.ExpirationTimeout));
policy.RemovedCallback = ItemRemoved;
break;
case ExpirationMode.Sliding:
policy.SlidingExpiration = item.ExpirationTimeout;
policy.RemovedCallback = ItemRemoved;
//// for some reason, we'll get issues with multithreading if we set this...
//// see http://stackoverflow.com/questions/21680429/why-does-memorycache-throw-nullreferenceexception
////policy.UpdateCallback = new CacheEntryUpdateCallback(ItemUpdated); // must be set, otherwise sliding doesn't work at all.
break;
}
item.LastAccessedUtc = DateTime.UtcNow;
return policy;
}
private string GetItemKey(CacheItem item) => GetItemKey(item?.Key, item?.Region);
private string GetItemKey(string key, string region = null)
{
Guard.NotNullOrWhiteSpace(key, nameof(key));
if (string.IsNullOrWhiteSpace(region))
{
return _instanceKey + ":" + key;
}
// key without region
// :key
// key with region
// @:
// @6region:key
return string.Concat(_instanceKey, "@", region.Length, "@", region, ":", key);
}
private string GetRegionTokenKey(string region)
{
string key = string.Concat(_instanceKey, "_", region);
return key;
}
private void InstanceTokenRemoved(CacheEntryRemovedArguments arguments)
{
_instanceKey = Guid.NewGuid().ToString();
_instanceKeyLength = _instanceKey.Length;
}
private void ItemRemoved(CacheEntryRemovedArguments arguments)
{
string fullKey = arguments.CacheItem.Key;
if (string.IsNullOrWhiteSpace(fullKey))
{
return;
}
// ignore manual removes, stats will be updated already
if (arguments.RemovedReason == CacheEntryRemovedReason.Removed)
{
return;
}
ParseKeyParts(_instanceKeyLength, fullKey, out bool isToken, out bool hasRegion, out string region, out string key);
if (isToken)
{
return;
}
if (hasRegion)
{
Stats.OnRemove(region);
}
else
{
Stats.OnRemove();
}
var item = arguments.CacheItem.Value as CacheItem;
object originalValue = null;
if (item != null)
{
originalValue = item.Value;
}
switch (arguments.RemovedReason)
{
// trigger cachemanager's remove on evicted and expired items
case CacheEntryRemovedReason.Evicted:
case CacheEntryRemovedReason.CacheSpecificEviction:
TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Evicted, originalValue);
break;
case CacheEntryRemovedReason.Expired:
TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Expired, originalValue);
break;
}
}
private static void ParseKeyParts(int instanceKeyLength, string fullKey, out bool isToken, out bool hasRegion, out string region, out string key)
{
string relevantKey = fullKey.Substring(instanceKeyLength);
isToken = relevantKey[0] == '_';
hasRegion = false;
region = null;
key = null;
if (isToken)
{
return;
}
hasRegion = relevantKey[0] == '@';
int regionLenEnd = hasRegion ? relevantKey.IndexOf('@', 1) : -1;
int regionLen;
regionLen = hasRegion && regionLenEnd > 0 ? int.TryParse(relevantKey.Substring(1, regionLenEnd - 1), out regionLen) ? regionLen : 0 : 0;
hasRegion = hasRegion && regionLen > 0;
string restKey = hasRegion ? relevantKey.Substring(regionLenEnd + 1) : relevantKey;
region = hasRegion ? restKey.Substring(0, regionLen) : null;
key = restKey.Substring(regionLen + 1);
}
}
}