alastairtree / LazyCache

An easy to use thread safe in-memory caching service with a simple developer friendly API for c#
https://nuget.org/packages/LazyCache
MIT License
1.72k stars 159 forks source link

RemovedCallback is not working correctly? #21

Closed RahmanM closed 6 years ago

RahmanM commented 7 years ago

Hi, Seems like the RemovedCallback is not working accurately. It is called in random intervals? For example I want to get a call back when the item is removed after the 5 seconds but somehow the callback happens in 11 seconds and sometimes in 20, 30 etc.

Am I missing anything?

Ta

`` class Program { static MemoryCache memoryCache = new MemoryCache("Program"); // Or??? static CachingService cache = new CachingService(memoryCache); // cache = new LazyCache.CachingService() { DefaultCacheDuration = 5}; ???

    public static void Main(string[] args)
    {

        var policy = new CacheItemPolicy()
        {
            Priority = CacheItemPriority.NotRemovable,
            AbsoluteExpiration =  DateTime.UtcNow.AddSeconds(5),
            RemovedCallback = OnCacheExpired,
        };

        cache.Add("1", "One", policy);
        Console.WriteLine("Adding to cache: " + DateTime.Now);

        //Console.WriteLine("Press any key to continue . . . ");
        Console.ReadKey(true);
    }

    private static void OnCacheExpired(CacheEntryRemovedArguments arguments)
    {
        var productsBeingRemovedFromCache = (String) arguments.CacheItem.Value;
        Console.WriteLine("Removing item from cahce: " + DateTime.Now);
        Console.WriteLine(cache.ObjectCache.GetCount());
    }

    private static void OnUpdateCallback(CacheEntryUpdateArguments arguments)
    {
        Console.WriteLine("Before removing=-> " + cache.ObjectCache.GetCount());
    }
}
alastairtree commented 7 years ago

There is an internal timer within MemoryCache that manages the removal polling. Have a look at this SO to see how you might go about reconfiguring it to your needs https://stackoverflow.com/q/12630168/3140853.

Ultimately the cache is designed optimise for performance rather than perfect timing of removals, since it is just cached data.

RahmanM commented 7 years ago

The fix is as suggested in SO.

I suggest this to be included inside LazyCache otherwise everyone else will have the same pain to go through.

Caching without invalidation doesn't make sense?

ie

static MemoryCache memoryCache = Create(); // static CachingService cache = new CachingService(memoryCache);

// This fixes the issue private static MemoryCache Create() { MemoryCache instance = null; Assembly assembly = typeof(CacheItemPolicy).Assembly; var type = assembly.GetType("System.Runtime.Caching.CacheExpires");

        if( type != null)
        {
            FieldInfo field = type.GetField("_tsPerBucket", BindingFlags.Static | BindingFlags.NonPublic);
            if(field != null && field.FieldType == typeof(TimeSpan))
            {
                TimeSpan originalValue = (TimeSpan)field.GetValue(null);
                field.SetValue(null, TimeSpan.FromSeconds(1));
                instance = new MemoryCache("FastExpiringCache");
                field.SetValue(null, originalValue); // reset to original value
            }
        }

        return instance ?? new MemoryCache("FastExpiringCache");
    }
alastairtree commented 6 years ago

In the upcoming version 2.0 of lazycache this is solved because the underlying Microft.Extensions.Caching.MemoryCache allows greater control of the expiration. The MemoryCacheOptions object now allows you to control the scan frequency of the cache cleanup, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions.expirationscanfrequency?view=aspnetcore-2.0

Be aware that there is no longer expiration based on timers, and so memory pressure or calling Get are now the triggers for expiration, however you can force time based expiration using cancellation tokens, see this SO answer for an example https://stackoverflow.com/a/47949111/3140853.

As we have a workaround for LazyCache v0.x and a fix for 2.0, I am going to close this issue.