feat: add per-account subscription result cache with LRU (Gap 5.4)
Implements RouteResultCache with fixed-capacity LRU eviction and atomic generation-based invalidation (Go ref: client.go routeCache, maxResultCacheSize=8192). Fixes AccountGoParityTests namespace ambiguity introduced by new test file.
This commit is contained in:
125
src/NATS.Server/Subscriptions/RouteResultCache.cs
Normal file
125
src/NATS.Server/Subscriptions/RouteResultCache.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
namespace NATS.Server.Subscriptions;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class RouteResultCache
|
||||
{
|
||||
private readonly int _capacity;
|
||||
private readonly Dictionary<string, LinkedListNode<(string Key, SubListResult Value)>> _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<string, LinkedListNode<(string Key, SubListResult Value)>>(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; } }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a subject's cached result.
|
||||
/// If at capacity, evicts the least recently used entry (tail of list).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments the generation counter so that the next TryGet detects a mismatch
|
||||
/// and clears stale entries. This is called whenever subscriptions change.
|
||||
/// </summary>
|
||||
public void Invalidate()
|
||||
{
|
||||
Interlocked.Increment(ref _generation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately clears all cached entries under the lock.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ClearUnderLock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearUnderLock()
|
||||
{
|
||||
_map.Clear();
|
||||
_list.Clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user