public sealed class PeCacheService : IPeCacheService
{
private readonly IMemoryCache _cache;
private readonly PeAnalysisOptions _options;
private readonly ConcurrentDictionary<string, Lazy<Task<PeImageInfo>>> _inflight = new();
private long _hits;
private long _misses;
public PeCacheService(IMemoryCache cache, IOptions<PeAnalysisOptions> options)
{
_cache = cache;
_options = options.Value;
}
public async Task<PeImageInfo> GetOrCreateAsync(
string key,
Func<CancellationToken, Task<PeImageInfo>> factory,
CancellationToken cancellationToken)
{
if (!_options.EnableCaching)
{
Interlocked.Increment(ref _misses);
return await factory(cancellationToken).ConfigureAwait(false);
}
if (_cache.TryGetValue<PeImageInfo>(key, out var existing) && existing is not null)
{
Interlocked.Increment(ref _hits);
return existing;
}
Interlocked.Increment(ref _misses);
var lazy = _inflight.GetOrAdd(
key,
static (_, state) =>
new Lazy<Task<PeImageInfo>>(
() => state.factory(state.ct),
LazyThreadSafetyMode.ExecutionAndPublication), (factory, ct: cancellationToken));
try
{
var value = await lazy.Value.ConfigureAwait(false);
_cache.Set(key, value, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _options.CacheTtl,
Size = 1
});
return value;
}
finally
{
_inflight.TryRemove(key, out _);
}
}
public CacheMetrics GetMetrics() => new(Interlocked.Read(ref _hits), Interlocked.Read(ref _misses));
}