1

I'm using MemoryCache in my asp.net application. After my application runs for hours, w3wp process consume so much memory, it can reach to 10 Gigabytes of memory and after while the application becomes unresponsive and IIS resets it automatically (by ping mechanism).

I've profiled the application process and find that MemoryCache create Multiple instances of MemoryCacheStore internally (equal to number of CPUs), and each instance consumes much memory!

it seems that for some reason MemoryCache stores same data in multiple instances and this causes huge memory consumption.

As you see in following image I have 16 CPUs and 16 of MemoryCacheStore is created internally by single MemoryCache instance. I've checked source code of MemoryCache and found that it creates an array of MemoryCacheStore of Environment.Processors count but didn't found that why all of this instances communes memory and how can prevent this? enter image description here

    public partial class MemoryCacheManager : DisposableObject, ICacheManager
{
    const string LockRecursionExceptionMessage = "Acquiring identical cache items recursively is not supported. Key: {0}";

    // Wwe put a special string into cache if value is null,
    // otherwise our 'Contains()' would always return false,
    // which is bad if we intentionally wanted to save NULL values.
    public const string FakeNull = "__[NULL]__";

    private readonly Work<ICacheScopeAccessor> _scopeAccessor;
    private MemoryCache _cache;
    private readonly ConcurrentDictionary<string, SemaphoreSlim> _keyLocks = new ConcurrentDictionary<string, SemaphoreSlim>();

    public MemoryCacheManager(Work<ICacheScopeAccessor> scopeAccessor)
    {
        _scopeAccessor = scopeAccessor;
        _cache = CreateCache();

    }

    private MemoryCache CreateCache()
    {
        return new MemoryCache("SmartStore" + DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss"));
    }

    public bool IsDistributedCache
    {
        get { return false; }
    }

    private bool TryGet<T>(string key, bool independent, out T value)
    {
        value = default(T);

        object obj = _cache.Get(key);

        if (obj != null)
        {
            // Make the parent scope's entry depend on this
            if (!independent)
            {
                _scopeAccessor.Value.PropagateKey(key);
            }

            if (obj.Equals(FakeNull))
            {
                return true;
            }

            value = (T)obj;
            return true;
        }

        return false;
    }

    public T Get<T>(string key, bool independent = false)
    {
        TryGet(key, independent, out T value);
        return value;
    }

    public T Get<T>(string key, Func<T> acquirer, TimeSpan? duration = null, bool independent = false)
    {
        if (TryGet(key, independent, out T value))
        {
            return value;
        }

        if (_scopeAccessor.Value.HasScope(key))
        {
            throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
        }

        // Get the (semaphore) locker specific to this key
        using (KeyedLock.Lock("cache:" + key, TimeSpan.FromMinutes(1)))
        {
            // Atomic operation must be outer locked
            if (!TryGet(key, independent, out value))
            {
                using (_scopeAccessor.Value.BeginScope(key))
                {
                    value = acquirer();
                    Put(key, value, duration, _scopeAccessor.Value.Current.Dependencies);
                    return value;
                }
            }
        }

        return value;
    }

    public async Task<T> GetAsync<T>(string key, Func<Task<T>> acquirer, TimeSpan? duration = null, bool independent = false)
    {
        if (TryGet(key, independent, out T value))
        {
            return value;
        }

        if (_scopeAccessor.Value.HasScope(key))
        {
            throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
        }

        // Get the async (semaphore) locker specific to this key
        using (await KeyedLock.LockAsync("cache:" + key, TimeSpan.FromMinutes(1)))
        {
            if (!TryGet(key, independent, out value))
            {
                using (_scopeAccessor.Value.BeginScope(key))
                {
                    value = await acquirer();
                    Put(key, value, duration, _scopeAccessor.Value.Current.Dependencies);
                    return value;
                }
            }
        }

        return value;
    }

    public void Put(string key, object value, TimeSpan? duration = null, IEnumerable<string> dependencies = null)
    {
        _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration, dependencies));
    }

    public bool Contains(string key)
    {
        return _cache.Contains(key);
    }

    public void Remove(string key)
    {
        _cache.Remove(key);
    }

    public IEnumerable<string> Keys(string pattern)
    {
        Guard.NotEmpty(pattern, nameof(pattern));

        var keys = _cache.AsParallel().Select(x => x.Key);

        if (pattern.IsEmpty() || pattern == "*")
        {
            return keys.ToArray();
        }

        var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase);
        return keys.Where(x => wildcard.IsMatch(x)).ToArray();
    }

    public int RemoveByPattern(string pattern)
    {
        lock (_cache)
        {
            var keysToRemove = Keys(pattern);
            int count = 0;

            // lock atomic operation
            foreach (string key in keysToRemove)
            {
                _cache.Remove(key);
                count++;
            }

            return count;
        }
    }

    public void Clear()
    {
        // Faster way of clearing cache: https://stackoverflow.com/questions/8043381/how-do-i-clear-a-system-runtime-caching-memorycache
        var oldCache = Interlocked.Exchange(ref _cache, CreateCache());
        oldCache.Dispose();
        GC.Collect();
    }

    public virtual ISet GetHashSet(string key, Func<IEnumerable<string>> acquirer = null)
    {
        var result = Get(key, () =>
        {
            var set = new MemorySet(this);
            var items = acquirer?.Invoke();
            if (items != null)
            {
                set.AddRange(items);
            }

            return set;
        });

        return result;
    }

    private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration, IEnumerable<string> dependencies)
    {
        var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration;

        if (duration.HasValue)
        {
            absoluteExpiration = DateTime.UtcNow + duration.Value;
        }

        var cacheItemPolicy = new CacheItemPolicy
        {
            AbsoluteExpiration = absoluteExpiration,
            SlidingExpiration = ObjectCache.NoSlidingExpiration
        };

        if (dependencies != null && dependencies.Any())
        {
            // INFO: we can only depend on existing items, otherwise this entry will be removed immediately.
            dependencies = dependencies.Where(x => x != null && _cache.Contains(x));
            if (dependencies.Any())
            {
                cacheItemPolicy.ChangeMonitors.Add(_cache.CreateCacheEntryChangeMonitor(dependencies));
            }
        }

        //cacheItemPolicy.RemovedCallback = OnRemoveEntry;

        return cacheItemPolicy;
    }

    //private void OnRemoveEntry(CacheEntryRemovedArguments args)
    //{
    //  if (args.RemovedReason == CacheEntryRemovedReason.ChangeMonitorChanged)
    //  {
    //      Debug.WriteLine("MEMCACHE: remove depending entry '{0}'.".FormatInvariant(args.CacheItem.Key));
    //  }
    //}

    protected override void OnDispose(bool disposing)
    {
        if (disposing)
            _cache.Dispose();
    }
}

// how i use cache

_requestCache.Get(key, () =>
        {
            var query = _categoryRepository.Table;

            if (!showHidden)
                query = query.Where(c => c.Published);

            query = query.Where(c => c.ParentCategoryId == parentCategoryId && !c.Deleted);

            query = ApplyHiddenCategoriesFilter(query, storeId, showHidden);

            var categories = query.OrderBy(x => x.DisplayOrder).ToList();
            return categories;
        });
r.zarei
  • 1,261
  • 15
  • 35
  • That's the job of a memory cache - to cache data in memory. If you use the wrong keys, or if you set the expiration to infinite, you'll end up writing every record in memory, never reusing anything, never removing stale data. – Panagiotis Kanavos Feb 10 '20 at 11:23
  • Post the code that adds items to `MemoryCache`,the code that uses it, the cache settings, *especially* the expiration and memory limits . The information posted here isn't useful - it describes what any kind of cache should be doing. – Panagiotis Kanavos Feb 10 '20 at 11:23
  • 1
    MemoryCache can be configured to use a maximum percentage of RAM, either [in code](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache.physicalmemorylimit?view=netframework-4.8#System_Runtime_Caching_MemoryCache_PhysicalMemoryLimit) or through [config settings](https://stackoverflow.com/questions/16678927/is-memorycache-scope-session-or-application-wide). [The answers to this probably duplicate question](https://stackoverflow.com/questions/16678927/is-memorycache-scope-session-or-application-wide) explains the defaults, eg *MIN* of 60% RAM or 1TB for x64 systems – Panagiotis Kanavos Feb 10 '20 at 11:34
  • The possible duplicate also points to a possible cause for the problem - creating too many MemoryCache instances, using the *default* limits. If the code creates 12 MemoryCache instances that can take 60% RAM each, well, they'll run out of RAM – Panagiotis Kanavos Feb 10 '20 at 11:37
  • @PanagiotisKanavos I've added my MemoryCacheManager class and added how i use it. note that i create a single instance of MemoryCacheManager for my application – r.zarei Feb 10 '20 at 11:52
  • *How* do you ensure there's only one instance? Why not use `MemoryCache.Default` instead of creating your own class? You haven't posted anything that shows what the expiration policy is, what the limits are or how the key is generated. This class essentially does what MemoryChace itself does. – Panagiotis Kanavos Feb 10 '20 at 11:55
  • @PanagiotisKanavos, I've checked name of instances by profiler. See constructor of my class, I've appended time of creation to the name of MemoryCacheInstance. Since i'v added some helper for MemoryCacheManager i'm using my implementation. – r.zarei Feb 10 '20 at 13:34
  • Here `var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration;`. This will keep items in memory forever. The call to `Get` doesn't contain a `duration` so the cache entry ends up using `InfiniteAbsoluteExpiration`. I'd suggest *not* using this code at all, get the cache to work with `MemoryCache.Default` and try to abstract that code only once the "hard-coded" implementation works. MemoryCache is [already thread-safe](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache?view=netframework-4.8#thread-safety), it doesn't need extra locking – Panagiotis Kanavos Feb 10 '20 at 13:39
  • Instead of hand-coding `TryGet` and adding, use one of the [AddOrGetExisting](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache.addorgetexisting?view=netframework-4.8) overloads. This will remove the need for locking – Panagiotis Kanavos Feb 10 '20 at 13:41
  • @PanagiotisKanavos, let me apply your suggestion, i will post the results. – r.zarei Feb 10 '20 at 13:43

0 Answers0