From 5f5c0f1cfbb2bfe5342a6f4a2da7820ef7899e52 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 4 Apr 2023 21:53:01 -0500 Subject: [PATCH] improve threading synchronization for date lookup cache --- Data/Helpers/DataValueCache.cs | 28 ++++-- Data/Helpers/LookupCache.cs | 167 +++++++++++++++------------------ 2 files changed, 98 insertions(+), 97 deletions(-) diff --git a/Data/Helpers/DataValueCache.cs b/Data/Helpers/DataValueCache.cs index 0765cf89d..ec4979acb 100644 --- a/Data/Helpers/DataValueCache.cs +++ b/Data/Helpers/DataValueCache.cs @@ -68,9 +68,14 @@ namespace Data.Helpers foreach (var id in ids) { - if (_cacheStates[key].ContainsKey(id)) + var cacheInstance = _cacheStates[key]; + + lock (_cacheStates) { - continue; + if (cacheInstance.ContainsKey(id)) + { + continue; + } } var state = new CacheState @@ -80,10 +85,12 @@ namespace Data.Helpers ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes) }; - _cacheStates[key].Add(id, state); + lock (_cacheStates) + { + cacheInstance.Add(id, state); + } _autoRefresh = autoRefresh; - if (!_autoRefresh || expirationTime == TimeSpan.MaxValue) { @@ -96,8 +103,8 @@ namespace Data.Helpers } } - public async Task GetCacheItem(string keyName, CancellationToken cancellationToken = default) => - await GetCacheItem(keyName, null, cancellationToken); + public Task GetCacheItem(string keyName, CancellationToken cancellationToken = default) => + GetCacheItem(keyName, null, cancellationToken); public async Task GetCacheItem(string keyName, object id = null, CancellationToken cancellationToken = default) @@ -107,7 +114,14 @@ namespace Data.Helpers throw new ArgumentException("No cache found for key {key}", keyName); } - var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id]; + var cacheInstance = _cacheStates[keyName]; + + CacheState state; + + lock (_cacheStates) + { + state = id is null ? cacheInstance.Values.First() : _cacheStates[keyName][id]; + } // when auto refresh is off we want to check the expiration and value // when auto refresh is on, we want to only check the value, because it'll be refreshed automatically diff --git a/Data/Helpers/LookupCache.cs b/Data/Helpers/LookupCache.cs index 1a6f06e4a..675f6733e 100644 --- a/Data/Helpers/LookupCache.cs +++ b/Data/Helpers/LookupCache.cs @@ -8,107 +8,94 @@ using System.Threading; using System.Threading.Tasks; using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace Data.Helpers +namespace Data.Helpers; + +public class LookupCache : ILookupCache where T : class, IUniqueId { - public class LookupCache : ILookupCache where T : class, IUniqueId + private readonly ILogger _logger; + private readonly IDatabaseContextFactory _contextFactory; + private Dictionary _cachedItems; + private readonly SemaphoreSlim _onOperation = new(1, 1); + + public LookupCache(ILogger> logger, IDatabaseContextFactory contextFactory) { - private readonly ILogger _logger; - private readonly IDatabaseContextFactory _contextFactory; - private Dictionary _cachedItems; - private readonly SemaphoreSlim _onOperation = new SemaphoreSlim(1, 1); + _logger = logger; + _contextFactory = contextFactory; + } - public LookupCache(ILogger> logger, IDatabaseContextFactory contextFactory) + public async Task AddAsync(T item) + { + await _onOperation.WaitAsync(); + T existingItem = null; + + if (_cachedItems.ContainsKey(item.Id)) { - _logger = logger; - _contextFactory = contextFactory; + existingItem = _cachedItems[item.Id]; } - public async Task AddAsync(T item) + if (existingItem != null) { - await _onOperation.WaitAsync(); - T existingItem = null; + _logger.LogDebug("Cached item already added for {Type} {Id} {Value}", typeof(T).Name, item.Id, + item.Value); + _onOperation.Release(); + return existingItem; + } - if (_cachedItems.ContainsKey(item.Id)) + try + { + _logger.LogDebug("Adding new {Type} with {Id} {Value}", typeof(T).Name, item.Id, item.Value); + await using var context = _contextFactory.CreateContext(); + context.Set().Add(item); + await context.SaveChangesAsync(); + _cachedItems.Add(item.Id, item); + return item; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not add item to cache for {Type}", typeof(T).Name); + throw new Exception("Could not add item to cache"); + } + finally + { + if (_onOperation.CurrentCount == 0) { - existingItem = _cachedItems[item.Id]; - } - - if (existingItem != null) - { - _logger.LogDebug("Cached item already added for {type} {id} {value}", typeof(T).Name, item.Id, - item.Value); _onOperation.Release(); - return existingItem; - } - - try - { - _logger.LogDebug("Adding new {type} with {id} {value}", typeof(T).Name, item.Id, item.Value); - await using var context = _contextFactory.CreateContext(); - context.Set().Add(item); - await context.SaveChangesAsync(); - _cachedItems.Add(item.Id, item); - return item; - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not add item to cache for {type}", typeof(T).Name); - throw new Exception("Could not add item to cache"); - } - finally - { - if (_onOperation.CurrentCount == 0) - { - _onOperation.Release(); - } - } - } - - public async Task FirstAsync(Func query) - { - await _onOperation.WaitAsync(); - - try - { - var cachedResult = _cachedItems.Values.Where(query); - - if (cachedResult.Any()) - { - return cachedResult.FirstOrDefault(); - } - } - - catch - { - } - - finally - { - if (_onOperation.CurrentCount == 0) - { - _onOperation.Release(1); - } - } - - return null; - } - - public IEnumerable GetAll() - { - return _cachedItems.Values; - } - - public async Task InitializeAsync() - { - try - { - await using var context = _contextFactory.CreateContext(false); - _cachedItems = await context.Set().ToDictionaryAsync(item => item.Id); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not initialize caching for {cacheType}", typeof(T).Name); } } } + + public async Task FirstAsync(Func query) + { + try + { + await _onOperation.WaitAsync(); + var cachedResult = _cachedItems.Values.Where(query); + return cachedResult.FirstOrDefault(); + } + finally + { + if (_onOperation.CurrentCount == 0) + { + _onOperation.Release(1); + } + } + } + + public IEnumerable GetAll() + { + return _cachedItems.Values; + } + + public async Task InitializeAsync() + { + try + { + await using var context = _contextFactory.CreateContext(false); + _cachedItems = await context.Set().ToDictionaryAsync(item => item.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not initialize caching for {CacheType}", typeof(T).Name); + } + } }