Pangamma icon

LruCacheWithTTL - dotnet10 - c#

Pangamma | PRO | 01/25/26 07:15:19 PM UTC | 0 ⭐ | 15950 👁️ | Never ⏰ | [lrucache]
C# |

5.14 KB

|

Source Code

|

0 👍

/

0 👎

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()+")";
            }
        }
    }
}
 

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎