caching.net-corememorycachelazycache

LazyCache: Regularly refresh cached items


I am using LazyCache and want to have cache refreshed e.g. every hour, but ideally I want the first caller after the cache item expired do not wait for cache reloaded. I wrote the following

public async Task<List<KeyValuePair<string, string>>> GetCarriersAsync()
{

    var options = new MemoryCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = new TimeSpan(1,0,0),// consider to config
    }.RegisterPostEvictionCallback(
         async  (key, value, reason, state) =>
        {
            await GetCarriersAsync();//will save to cache
            _logger.LogInformation("Carriers are reloaded: " );
        });
    Func<Task<List<KeyValuePair<string, string>>>> cacheableAsyncFunc = () => GetCarriersFromApi();
    var cachedCarriers = await _cache.GetOrAddAsync($"Carriers", cacheableAsyncFunc, options);

    return cachedCarriers;
}

However RegisterPostEvictionCallback is not called when cache item is expired, but only when the next request to the item occurred (and the caller need to wait for a lengthy operation).

The thread Expiration almost never happens on it's own in the background #248 explains that this is by design, and suggests workaround to specify CancellationTokenSource.CancelAfter(TimeSpan.FromHours(1)) instead of SetAbsoluteExpiration.

Unfortunately LazyCache.GetOrAddAsync doesn’t have CancellationToken as a parameter. What is the best way to trigger reload of cache on a scheduled time with minimal waiting time for the first user?


Solution

  • I found the similar question In-Memory Caching with auto-regeneration on ASP.Net Core that suggested to call the AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(_options.ReferenceDataRefreshTimeSpan).Token).

    I tried it, but didn't make it working. However the same answer had alternative(and recommended) option by using timer. I've created a class RefreshebleCache that I am using for different cachable options like the following:

       var refreshebleCache = new RefreshebleCache<MyCashableObjectType>(_cache, _logger);
       Task<MyCashableObjectType> CacheableAsyncFunc() => GetMyCashableObjectTypeFromApiAsync();
       var cachedResponse = await refreshebleCache.GetOrAddAsync("MyCashableObject", CacheableAsyncFunc,
                            _options.RefreshTimeSpan);
    

    The RefreshebleCache implementation:

    /// <summary>
        /// Based on https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public class RefreshebleCache<T>
        {
    
            protected readonly IAppCache _cache;
            private readonly ILogger _logger;
            public bool LoadingBusy = false;
            private string _cacheKey;
            private TimeSpan _refreshTimeSpan;
            private Func<Task<T>> _functionToLoad;
            private Timer _timer;
    
            public RefreshebleCache(IAppCache cache, ILogger logger)
            {
    
                _cache = cache;
                _logger = logger;
            }
    
            public async Task<T>  GetOrAddAsync (string cacheKey , Func<Task<T>> functionToLoad, TimeSpan refreshTimeSpan)
            {
                _refreshTimeSpan= refreshTimeSpan;
                _functionToLoad = functionToLoad;
                _cacheKey = cacheKey;
                var timerCachedKey = "Timer_for_"+cacheKey;
                //if removed from cache, _timer could continue to work, creating redundant calls
                _timer =  _appCache.GetOrAdd(timerCachedKey, () => 
                 CreateTimer(refreshTimeSpan), 
      SetMemoryCacheEntryOptions(CacheItemPriority.NeverRemove));
                var cachedValue = await LoadCacheEntryAsync();
                return  cachedValue;
            }
            private Timer CreateTimer(TimeSpan refreshTimeSpan)
            {
                Debug.WriteLine($"calling CreateTimer for {_cacheKey} refreshTimeSpan {refreshTimeSpan}"); //start first time in refreshTimeSpan
                return new Timer(TimerTickAsync, null, refreshTimeSpan, refreshTimeSpan);
            }
    
        
            private async void TimerTickAsync(object state)
            {
                if (LoadingBusy) return;
                try
                {
                    LoadingBusy = true;
                    Debug.WriteLine($"calling LoadCacheEntryAsync from TimerTickAsync for {_cacheKey}");
                    var loadingTask = LoadCacheEntryAsync(true);
                    await loadingTask;
                }
                catch(Exception e)
                {
                    _logger.LogWarning($" {nameof(T)} for {_cacheKey} was not reloaded.    {e} ");
                }
                finally
                {
                    LoadingBusy = false;
                }
            }
            private async Task<T> LoadCacheEntryAsync(bool update=false)
            {
                var cacheEntryOptions = SetMemoryCacheEntryOptions();
    
                Func<Task<T>> cacheableAsyncFunc = () => _functionToLoad();
                Debug.WriteLine($"called LoadCacheEntryAsync for {_cacheKey} update:{update}");
                T cachedValues = default(T);
                if (update)
                {
                    cachedValues =await cacheableAsyncFunc();
                    if (cachedValues != null)
                    {
                        _cache.Add(_cacheKey, cachedValues, cacheEntryOptions);
                    }
    
                    //    _cache.Add(_cacheKey, cacheableAsyncFunc, cacheEntryOptions);
                }
                else
                {
                     cachedValues = await _cache.GetOrAddAsync(_cacheKey, cacheableAsyncFunc, cacheEntryOptions);
                }
                return cachedValues;
            }
            private MemoryCacheEntryOptions SetMemoryCacheEntryOptions(CacheItemPriority priority= CacheItemPriority.Normal)
           {
              var cacheEntryOptions = new MemoryCacheEntryOptions
              {
                Priority = priority 
              };
              return cacheEntryOptions;
            }
    
     }
    

    }