namespace NATS.Server.Auth; /// /// Fixed-capacity LRU cache for permission results, supporting both PUB and SUB entries /// with generation-based invalidation. /// Lock-protected (per-client, low contention). /// Reference: Go client.go maxPermCacheSize=128. /// public sealed class PermissionLruCache { private readonly int _capacity; private readonly Dictionary> _map; private readonly LinkedList<(string Key, bool Value)> _list = new(); private readonly object _lock = new(); // Generation tracking: _generation is the authoritative counter (bumped by Invalidate), // _cacheGeneration is what the cache was last synced to. A mismatch on access triggers a clear. private long _generation; private long _cacheGeneration; public PermissionLruCache(int capacity = 128) { _capacity = capacity; _map = new Dictionary>(capacity, StringComparer.Ordinal); } /// /// The current generation counter. Incremented on each call. /// public long Generation => Interlocked.Read(ref _generation); /// /// Bumps the generation counter so that the next TryGet or TryGetSub call detects /// the staleness and clears all cached entries. /// public void Invalidate() => Interlocked.Increment(ref _generation); // Ensures the cache is cleared if Invalidate was called since the last access. // Must be called inside _lock. private void EnsureFresh() { var gen = Interlocked.Read(ref _generation); if (gen != _cacheGeneration) { _map.Clear(); _list.Clear(); _cacheGeneration = gen; } } // ── PUB API (backward-compatible) ──────────────────────────────────────── /// Looks up a PUB permission for . public bool TryGet(string key, out bool value) { var internalKey = "P:" + key; lock (_lock) { EnsureFresh(); if (_map.TryGetValue(internalKey, out var node)) { value = node.Value.Value; _list.Remove(node); _list.AddFirst(node); return true; } value = default; return false; } } /// Stores a PUB permission for . public void Set(string key, bool value) { var internalKey = "P:" + key; lock (_lock) { EnsureFresh(); SetInternal(internalKey, value); } } // ── SUB API ─────────────────────────────────────────────────────────────── /// Looks up a SUB permission for . public bool TryGetSub(string subject, out bool value) { var internalKey = "S:" + subject; lock (_lock) { EnsureFresh(); if (_map.TryGetValue(internalKey, out var node)) { value = node.Value.Value; _list.Remove(node); _list.AddFirst(node); return true; } value = default; return false; } } /// Stores a SUB permission for . public void SetSub(string subject, bool allowed) { var internalKey = "S:" + subject; lock (_lock) { EnsureFresh(); SetInternal(internalKey, allowed); } } // ── Shared ──────────────────────────────────────────────────────────────── public int Count { get { lock (_lock) { return _map.Count; } } } // Must be called inside _lock. private void SetInternal(string internalKey, bool value) { if (_map.TryGetValue(internalKey, out var existing)) { _list.Remove(existing); existing.Value = (internalKey, value); _list.AddFirst(existing); return; } if (_map.Count >= _capacity) { var last = _list.Last!; _map.Remove(last.Value.Key); _list.RemoveLast(); } var node = new LinkedListNode<(string Key, bool Value)>((internalKey, value)); _list.AddFirst(node); _map[internalKey] = node; } }