.Net Core MemoryCache PostEvictionCallback not working properly

11,135

Solution 1

It is happening because the item is not evicted till you query for the item and it checks the expiration

(From the Source of MemoryCacheStore.Get(MemoryCacheKey key))

    internal MemoryCacheEntry Get(MemoryCacheKey key) {
        MemoryCacheEntry entry = _entries[key] as MemoryCacheEntry;
        // has it expired?
        if (entry != null && entry.UtcAbsExp <= DateTime.UtcNow) {
            Remove(key, entry, CacheEntryRemovedReason.Expired);
            entry = null;
        }
        // update outside of lock
        UpdateExpAndUsage(entry);
        return entry;
    }

or when Trim() is called internally due to memory pressure

(From the Source of TrimInternal(int percent))

/*SNIP*/
        trimmedOrExpired = _expires.FlushExpiredItems(true);
        if (trimmedOrExpired < toTrim) {
            trimmed = _usage.FlushUnderUsedItems(toTrim - trimmedOrExpired);
            trimmedOrExpired += trimmed;
        }
/*SNIP*/

If your system is not currently low enough on memory to trigger a trim then the only time items will be evicted is when they are attempted to be retrieved.

Solution 2

To add onto the accept answer and comments, you can force the cache to expire and evict automatically by using a expiring cancellation token.

int expirationMinutes = 60;
var expirationTime = DateTime.Now.Add(expirationMinutes);
var expirationToken = new CancellationChangeToken(
    new CancellationTokenSource(TimeSpan.FromMinutes(expirationMinutes + .01)).Token);

var cacheEntryOptions = new MemoryCacheEntryOptions()
         // Pin to cache.
         .SetPriority(CacheItemPriority.NeverRemove)
         // Set the actual expiration time
         .SetAbsoluteExpiration(expirationTime)
         // Force eviction to run
         .AddExpirationToken(expirationToken)
         // Add eviction callback
         .RegisterPostEvictionCallback(callback: CacheItemRemoved, state: this); 

`

The lack of built in timer behavior, which the old one used to have, is supposed to be by design and this is what was recommended in its place. See: https://github.com/aspnet/Caching/issues/248

Share:
11,135

Related videos on Youtube

mattinsalto
Author by

mattinsalto

Updated on June 04, 2022

Comments

  • mattinsalto
    mattinsalto almost 2 years

    I have set cache items with sliding expiration in a Microsoft.Extensions.Caching.Memory.MemoryCache. I want to trigger a callback everytime a cache item expires, but callback isn't triggered until I query the cache for the expired cache item.

    Here is the code:

    using System;
    using Microsoft.Extensions.Caching.Memory;
    
    namespace Memcache
    {
        public class Program
        {
            private static MemoryCache _cache;
            private static int _cacheExpSecs;
    
            public static void Main(string[] args)
            {
                _cache = new MemoryCache(new MemoryCacheOptions());
                _cacheExpSecs = 2;
    
                var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromSeconds(_cacheExpSecs))
                .RegisterPostEvictionCallback(callback: EvictionCallback);
    
                _cache.Set(1, "One", cacheEntryOptions);
                _cache.Set(2, "Two", cacheEntryOptions);
    
                var autoEvent = new System.Threading.AutoResetEvent(false);
    
                System.Threading.Timer timer = new System.Threading.Timer(checkCache, autoEvent, 1000, 6000);
    
                Console.Read();
            }
    
            private static void checkCache(Object o)
            {
                if(_cache.Get(1)!=null)
                {
                    Console.WriteLine(string.Format(@"checkCache: Cache with key {0} will be removed manually and will trigger the callback.", 1));
                    _cache.Remove(1);
                }
                else
                {
                    Console.WriteLine(string.Format("checkCache: Cache with key {0} is expired.", 1));
                }
    
    
                if(_cache.Get(2) != null)
                {
                    Console.WriteLine(string.Format("checkCache: Cache with key {0} will expire in {1} seconds, but won't trigger the callback until we check it's value again.", 2, _cacheExpSecs));
                }
                else
                {
                    Console.WriteLine(string.Format("checkCache: Cache with key {0} is expired.", 2));
                }
    
            }
    
            private static void EvictionCallback(object key, object value, EvictionReason reason, object state)
            {
                Console.WriteLine();
                Console.WriteLine("/*****************************************************/");
                Console.WriteLine(string.Format("/*  EvictionCallback: Cache with key {0} has expired.  */", key));
                Console.WriteLine("/*****************************************************/");
                Console.WriteLine();
            }
        }
    }
    
  • mattinsalto
    mattinsalto about 7 years
    Thanks. There is ExpirationScanFrequency option in MemoryCacheOptions, but neither works.
  • Scott Chamberlain
    Scott Chamberlain about 7 years
    You might want to unaccept my answer for now. I totally overlooked this was .NET Core. Checking the Core source now that my statements are still correct
  • Scott Chamberlain
    Scott Chamberlain about 7 years
    Ok, the logic is still the same. ExpirationScanFrequency is the frequency it does a full scan after any kind of Get or Remove is peformed. If the time has been longer than the ExpirationScanFrequency it does a full scan of items instead of just the one it was working with, It still does not run a timer to perform the scans, they are all still done on demand when a action is perfomed.
  • Scott Chamberlain
    Scott Chamberlain about 7 years
    If you wanted to you could call _cache.Compact(0) on your own timer and that would flush out the expired entries on a regular basis.
  • mattinsalto
    mattinsalto about 7 years
    Thank you, I'll use Compact(0). On the other hand, in .NET framework, System.Runtime.Caching.MemoryCache triggers the callback when sliding expiration cache item is expired.
  • Marco
    Marco almost 4 years
    Thanks, this was exactly what I was looking for and can confirm that cache does not need to be hit for this to trigger.