LazyCache: регулярно обновлять кэшированные элементы - PullRequest
0 голосов
/ 02 июня 2019

Я использую LazyCache и хочу, чтобы кэш обновлялся, например, каждый час, но в идеале я хочу, чтобы первый абонент после истечения срока действия элемента кеша не ждал перезагрузки кеша.Я написал следующее

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

Однако RegisterPostEvictionCallback не вызывается, когда истекает срок действия элемента кэша, а только тогда, когда происходит следующий запрос к элементу (и вызывающей стороне нужно ждать длительной операции).

Поток Истечение срока действия почти никогда не происходит само по себе в фоновом режиме # 248 объясняет, что это по замыслу, и предлагает обходной путь, чтобы вместо него указать CancellationTokenSource.CancelAfter (TimeSpan.FromHours (1))of SetAbsoluteExpiration.

К сожалению, LazyCache.GetOrAddAsync не имеет CancellationToken в качестве параметра.Каков наилучший способ запуска перезагрузки кэша в запланированное время с минимальным временем ожидания для первого пользователя?

1 Ответ

0 голосов
/ 08 июня 2019

Я нашел похожий вопрос Кэширование в памяти с авторегенерацией на ASP.Net Core , в котором предлагалось вызвать AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(_options.ReferenceDataRefreshTimeSpan).Token).

Я попробовал, но не помог. Однако тот же ответ имел альтернативный (и рекомендуемый) вариант с использованием таймера. Я создал класс RefreshebleCache, который я использую для различных кэшируемых опций, таких как:

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

Реализация RefreshebleCache:

 /// <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;
             _timer =  _cache.GetOrAdd("Timer_for_"+cacheKey, () => CreateTimer(refreshTimeSpan));
            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()
        {
            //Make  Expiration a bit bigger( =1.1) than _refreshTimeSpan, to let refresh before expiry
            TimeSpan expirationTimeSpan = new TimeSpan(Convert.ToInt64(_refreshTimeSpan.Ticks * 1.1));
            var cacheEntryOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = expirationTimeSpan
            };
            //I've attempted to use AddExpirationToken and RegisterPostEvictionCallback, but wasn't able to invoke reliably.
            // Instead timer from https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core seems working
            //.AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(_refreshTimeSpan).Token))
            //.RegisterPostEvictionCallback(
            //    async (key, value, reason, state) =>
            //    {
            //        await GetOrAddAsync(_cacheKey,  _functionToLoad, _refreshTimeSpan);//suppose to save to cache
            //        _logger.LogInformation($" {nameof(T)} are reloaded  ");
            //    });
            return cacheEntryOptions;
        }
...