namespace NATS.Server.Subscriptions;
///
/// Per-account LRU cache for subscription match results.
/// Avoids repeated SubList.Match() calls for the same subject.
/// Uses atomic generation ID for bulk invalidation when subscriptions change.
/// Go reference: server/client.go routeCache, maxResultCacheSize=8192.
///
public sealed class RouteResultCache
{
private readonly int _capacity;
private readonly Dictionary> _map;
private readonly LinkedList<(string Key, SubListResult Value)> _list = new();
private readonly Lock _lock = new();
private long _generation;
private long _cacheGeneration;
// Stats
private long _hits;
private long _misses;
public RouteResultCache(int capacity = 8192)
{
_capacity = capacity;
_map = new Dictionary>(capacity, StringComparer.Ordinal);
}
public long Generation => Interlocked.Read(ref _generation);
public long Hits => Interlocked.Read(ref _hits);
public long Misses => Interlocked.Read(ref _misses);
public int Count { get { lock (_lock) return _map.Count; } }
///
/// Attempts to retrieve a cached result for the given subject.
/// On generation mismatch (subscriptions changed since last cache fill),
/// clears all entries and returns false.
/// Updates LRU order on a hit.
///
public bool TryGet(string subject, out SubListResult? result)
{
lock (_lock)
{
var currentGeneration = Interlocked.Read(ref _generation);
if (_cacheGeneration != currentGeneration)
{
ClearUnderLock();
_cacheGeneration = currentGeneration;
result = null;
Interlocked.Increment(ref _misses);
return false;
}
if (_map.TryGetValue(subject, out var node))
{
// Move to front (most recently used)
_list.Remove(node);
_list.AddFirst(node);
result = node.Value.Value;
Interlocked.Increment(ref _hits);
return true;
}
}
result = null;
Interlocked.Increment(ref _misses);
return false;
}
///
/// Adds or updates a subject's cached result.
/// If at capacity, evicts the least recently used entry (tail of list).
///
public void Set(string subject, SubListResult result)
{
lock (_lock)
{
if (_map.TryGetValue(subject, out var existing))
{
// Update value in place and move to front
_list.Remove(existing);
var updated = new LinkedListNode<(string Key, SubListResult Value)>((subject, result));
_list.AddFirst(updated);
_map[subject] = updated;
return;
}
// Evict LRU tail if at capacity
if (_map.Count >= _capacity && _list.Last is { } tail)
{
_map.Remove(tail.Value.Key);
_list.RemoveLast();
}
var node = new LinkedListNode<(string Key, SubListResult Value)>((subject, result));
_list.AddFirst(node);
_map[subject] = node;
}
}
///
/// Increments the generation counter so that the next TryGet detects a mismatch
/// and clears stale entries. This is called whenever subscriptions change.
///
public void Invalidate()
{
Interlocked.Increment(ref _generation);
}
///
/// Immediately clears all cached entries under the lock.
///
public void Clear()
{
lock (_lock)
{
ClearUnderLock();
}
}
private void ClearUnderLock()
{
_map.Clear();
_list.Clear();
}
}