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); } } }