I need my cache hold items to be accessible by key. The items should disappear after a configured time period. I do not need the lifetime of items to be exactly as configured. All I need is to make sure that the cache does not grow infinitely and that items are accessible for at least the given time. The rate of items coming is variable, but sensibly limited. The memory size of items is small.
The cache must be thread safe.
Traditional implementation of a cache with eviction would probably involve maintaining some kind of a queue as an addition to a dictionary. I aimed to avoid complexity related to maintaining two data structures.
See my code below.
public class InMemScanItemCache: IScanItemCache, IDisposable
{
private readonly ITimer _timer;
ConcurrentDictionary<long, ScannedItem> _activeCache = new();
ConcurrentDictionary<long, ScannedItem> _pastCache = new();
public InMemScanItemCache(ITimer timer, IOptions<ScannerInputConfig> options)
{
_timer = timer;
_timer.Elapsed += OnTimedEvent;
_timer.AutoReset = true;
_timer.Interval = options.Value.RetainCachedItemsForSec * 1000;
_timer.Enabled = true;
}
public Task SaveAsync(ScannedItem item)
{
_activeCache[item.ItemId] = item;
return Task.CompletedTask;
}
public Task<ScannedItem?> GetByItemIdAsync(long itemId)
{
_activeCache.TryGetValue(itemId, out var item);
if(item == null)
_pastCache.TryGetValue(itemId, out item);
return Task.FromResult(item);
}
private void OnTimedEvent(Object? source, System.Timers.ElapsedEventArgs e)
{
_pastCache = _activeCache;
_activeCache = new ConcurrentDictionary<long, ScannedItem>();
}
public void Dispose()
{
_timer.Enabled = false;
_timer.Elapsed -= OnTimedEvent;
_timer.Close();
}
}
Note 1:
The save and get methods are async because IScanItemCache
interface requires that (there may be other implementations that are actually async).
Note 2:
ITimer
interface exactly repeats System.Timers.Timer
class API. The interface was introduced for testability, and production code will use this trivial implementation:
public class RealTimer: System.Timers.Timer, ITimer
{
// All the interface members are already implemented in the base class
}
The RealTimer
will be registered in DI as a transient service.
My concern is: is the implementation really thread-safe? Is the memory usage OK? Is the timer usage pattern OK?