mattinsalto
mattinsalto

Reputation: 2356

.Net Core MemoryCache PostEvictionCallback not working properly

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

Upvotes: 29

Views: 18061

Answers (2)

Steve Kinyon
Steve Kinyon

Reputation: 875

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

Upvotes: 56

Scott Chamberlain
Scott Chamberlain

Reputation: 127543

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.

Upvotes: 21

Related Questions