Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using System;
- using System.Collections.Generic;
- using System.Text;
- namespace DiscordTranslator.Bot.Logic
- {
- /// <summary>
- /// A Least Recently Used cache implementation with forced expiry logic added on.
- /// https://stackoverflow.com/a/3719378/1582837
- /// https://searchstorage.techtarget.com/definition/cache-algorithm
- /// Any extra questions just ask Taylor Love.
- /// </summary>
- /// <typeparam name="K"></typeparam>
- /// <typeparam name="V"></typeparam>
- public class LruCacheWithTTL<K, V> where K : notnull where V : notnull
- {
- private readonly int _capacity;
- private readonly TimeSpan? _maxTTL;
- private readonly Dictionary<K, LinkedListNode<LRUCacheItem>> _map = [];
- private readonly LinkedList<LRUCacheItem> _lru = new();
- private readonly Lock _sync = new();
- public int Count
- {
- get { lock (_sync) { return _lru.Count; } }
- }
- public LruCacheWithTTL(int capacity)
- {
- this._capacity = capacity;
- _maxTTL = null;
- }
- public LruCacheWithTTL(int capacity, TimeSpan maxTTL)
- {
- this._capacity = capacity;
- this._maxTTL = maxTTL;
- }
- /// <summary>
- /// Retrieves the value by key and then attempts to cast to type T. Returns
- /// default(T) if value not in cache or value is not of needed type.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="key"></param>
- /// <returns></returns>
- public bool TryGetValue<T>(K key, out T? val) where T : V
- {
- val = default(T);
- lock (_sync)
- {
- if (_map.TryGetValue(key, out var node))
- {
- if (node.Value.ForceExpireAt != null && node.Value.ForceExpireAt.Value < DateTime.UtcNow)
- {
- _lru.Remove(node);
- _map.Remove(key);
- return false;
- }
- _lru.Remove(node);
- _lru.AddLast(node);
- if (node.Value.Value is T t)
- {
- val = t;
- return true;
- }
- }
- val = default;
- return false;
- }
- }
- public bool Clear()
- {
- lock (_sync)
- {
- _map.Clear();
- _lru.Clear();
- }
- return true;
- }
- public void Set(K key, V value, TimeSpan? maxTTL = null)
- {
- TimeSpan? ttl = maxTTL ?? this._maxTTL;
- DateTime? expiry = ttl.HasValue ? DateTime.UtcNow.Add(ttl.Value) : null;
- lock (_sync)
- {
- if (_map.TryGetValue(key, out var existing))
- {
- existing.Value = existing.Value with
- {
- Value = value,
- ForceExpireAt = expiry
- };
- // Refresh LRU position
- _lru.Remove(existing);
- _lru.AddLast(existing);
- }
- else
- {
- if (_map.Count >= _capacity)
- RemoveFirst(); // evict LRU
- LRUCacheItem item = new LRUCacheItem(key, value, expiry);
- var node = new LinkedListNode<LRUCacheItem>(item);
- _lru.AddLast(node);
- _map[key] = node;
- }
- }
- }
- public void Remove(K key)
- {
- lock (_sync)
- {
- if (_map.TryGetValue(key, out var node))
- {
- _lru.Remove(node); // Removal from LinkedList is O(1) if you have direct link to the Node.
- _map.Remove(key);
- if (node.Value is IDisposable dispVal)
- {
- dispVal.Dispose();
- }
- }
- }
- }
- private void RemoveFirst()
- {
- // Evict least recently used (front)
- if (_lru.First is { } first)
- {
- _lru.RemoveFirst();
- // Remove from map using the evicted key
- _map.Remove(first.Value.Key);
- if (first.Value is IDisposable dispVal)
- {
- dispVal.Dispose();
- }
- }
- }
- internal sealed record LRUCacheItem(K Key, V Value, DateTime? ForceExpireAt)
- {
- public override string ToString()
- {
- if (ForceExpireAt == null)
- {
- return $"{Key} => {Value} (No expiry)";
- }
- var timeRemaining = DateTime.UtcNow - ForceExpireAt.Value;
- return $"{Key} => {Value} (Expires: "+timeRemaining.ToString()+")";
- }
- }
- }
- }
Advertisement