From bc82b2dc2f7405c0fd4d179830412ea8209137b1 Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Wed, 27 Sep 2023 03:38:58 +1000 Subject: Added MemoryCache class and implemented principal/ACL cache in the security service --- Util/MemoryCache.cs | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 Util/MemoryCache.cs (limited to 'Util/MemoryCache.cs') diff --git a/Util/MemoryCache.cs b/Util/MemoryCache.cs new file mode 100644 index 0000000..ba314cf --- /dev/null +++ b/Util/MemoryCache.cs @@ -0,0 +1,98 @@ +using System.Collections; + +namespace HyperBooru.Util; + +public class MemoryCache : IEnumerable> where TKey : struct { + /// Maximum number of items this cache may hold (unlimited if null) + public int? MaxItems { get; init; } + /// Maximum amount of time an item may stay in cache before it is removed (unlimited if null) + public TimeSpan? MaxAge { get; init; } + /// Function that will be called to populate the cache in the event of a cache-miss + public Func? DataSource { get; init; } + + private Dictionary cache = new(); + + public TValue this[TKey key] { + get => GetValue(key); + set { + Prune(); + cache[key] = (DateTime.Now, value); + } + } + + public TValue GetValue(TKey key) { + bool success = cache.TryGetValue(key, out var result); + if(success) { + if(MaxAge is null) + return result.value; + if(result.createTime > DateTime.Now - MaxAge) + return result.value; + } + + if(DataSource is null) + throw new KeyNotFoundException(); + + TValue? value = DataSource(key); + + if(value is null) + throw new KeyNotFoundException(); + + Prune(); + cache[key] = (DateTime.Now, value); + return value; + } + + public bool TryGetValue(TKey key, out TValue? value) { + try { + value = GetValue(key); + return true; + } catch(KeyNotFoundException) { + value = default; + return false; + } + } + + public bool Remove(TKey key) => cache.Remove(key); + + public IEnumerator> GetEnumerator() { + DateTime? expiry = MaxAge is null ? null : DateTime.Now - MaxAge; + + foreach(var kv in cache) { + // Don't return expired cache items + if(expiry is not null) + if(kv.Value.createTime < expiry) + continue; + + yield return new KeyValuePair(kv.Key, kv.Value.value); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private void Prune() { + DateTime? expiry = MaxAge is null ? null : DateTime.Now - MaxAge; + + // If an expiry time for cache items was + // specified, remove expired cache items. + if(expiry is not null) { + foreach(var kv in cache) { + if(kv.Value.createTime < expiry) + cache.Remove(kv.Key); + } + } + + // If this cache was created with a maximum size, + // remove elements until that size is reached. + if(MaxItems is null || cache.Count() < MaxItems) + return; + + var toRemove = cache + .OrderBy(kv => kv.Value.createTime) + .Take(cache.Count() - (int) MaxItems + 1) + .Select(kv => kv.Key) + .ToArray(); + + foreach(var key in toRemove) + cache.Remove(key); + } +} -- cgit v1.3