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

LongRunning shouldn't run more than once #173

Open laukoksoon opened 2 years ago

laukoksoon commented 2 years ago

Describe the bug My goal here is to cache some long running process data and then upon expiration, i will get those data that going to expired and append new delta changes from database The long running process shouldn't run more than one time

To Reproduce I will attach the console program that i wrote to reproduce the issue

When i set int parallelNumber = 100; // 100 or below => All the thing run as EXPECTED

When i set int parallelNumber = 300; // 300 or allow => Long process called more than once .. [NOT OK]

Expected behavior Long process should call ONE even the parallel count set to 300 above

** Framework and Platform

Console source code

using LazyCache; using Microsoft.Extensions.Caching.Memory; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks;

namespace ConsoleApp1 { class program { private static IAppCache _lazyCache; private static int _totalLongProcess; private static int _totalAppendProcess;

    static List<int> LongProcess(string key)
    {
        _totalLongProcess++;

        if (_totalLongProcess > 1)
        {
            throw new Exception("Not suppose to run more than one time");
        }

        Console.WriteLine("LONG PROCESS for key " + key);
        Thread.Sleep(5 * 1000);
        return Enumerable.Range(0, 100).ToList();
    }

    static List<int> AppendElement(string key, List<int> ori)
    {
        _totalAppendProcess++;
        Console.WriteLine("Append Process for key " + key);
        Thread.Sleep(2 * 1000);

        var newList = new List<int>();
        newList.Add(12345);
        newList.Add(54321);

        ori.AddRange(newList);
        return ori;
    }

    static List<int> GetCacheByKey(string key, string threadId)
    {
        Console.WriteLine($"Request key {key}, threadId {threadId}");
        var result = _lazyCache.GetOrAdd(key, () => LongProcess(key), GetOptions());
        return result;
    }

    static MemoryCacheEntryOptions GetOptions()
    {
        //ensure the cache item expires exactly on 30s (and not lazily on the next access)
        var options = new LazyCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromSeconds(10), ExpirationMode.ImmediateExpiration);

        // as soon as it expires, re-add it to the cache
        options.RegisterPostEvictionCallback((keyEvicted, value, reason, state) =>
        {
            // dont re-add if running out of memory or it was forcibly removed
            if (reason == EvictionReason.Expired || reason == EvictionReason.TokenExpired)
            {
                var oriList = value as List<int>;
                _lazyCache.GetOrAdd(keyEvicted.ToString(), _ => AppendElement(keyEvicted.ToString(), oriList), GetOptions()); //calls itself to get another set of options!
            }
        });
        return options;
    }

    static void Main(string[] args)
    {
        _lazyCache = new CachingService();

        int round = 1;
        int i = 0;
        while (i < round)
        {
            int parallelNumber = 300;
            Parallel.For(0, parallelNumber, count =>
            {
                Thread.Sleep(1 * 1000);
                var list = GetCacheByKey("1", Thread.CurrentThread.ManagedThreadId.ToString());
                Console.WriteLine($"Got result for key {"1"}, threadId {Thread.CurrentThread.ManagedThreadId.ToString()}, count {list.Count}");
            });

            i++;
        }

        Console.WriteLine("Total long process run: " + _totalLongProcess);
        Console.WriteLine("Total append process run: " + _totalAppendProcess);
        Console.ReadLine();
    }
}

}

alastairtree commented 2 years ago

How long does the 300 version take? Your code only has 10s (not 30s as commented) - could it be longer than 10?

Also your expiration code is wrong - it should be using DateTiemOffset

new LazyCacheEntryOptions().SetAbsoluteExpiration(DateTimeOffset.Now.AddSeconds(10))