feat: add PermissionLruCache (128-entry LRU) and wire into ClientPermissions
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using NATS.Server.Subscriptions;
|
using NATS.Server.Subscriptions;
|
||||||
|
|
||||||
namespace NATS.Server.Auth;
|
namespace NATS.Server.Auth;
|
||||||
@@ -7,7 +6,7 @@ public sealed class ClientPermissions : IDisposable
|
|||||||
{
|
{
|
||||||
private readonly PermissionSet? _publish;
|
private readonly PermissionSet? _publish;
|
||||||
private readonly PermissionSet? _subscribe;
|
private readonly PermissionSet? _subscribe;
|
||||||
private readonly ConcurrentDictionary<string, bool> _pubCache = new(StringComparer.Ordinal);
|
private readonly PermissionLruCache _pubCache = new(128);
|
||||||
|
|
||||||
private ClientPermissions(PermissionSet? publish, PermissionSet? subscribe)
|
private ClientPermissions(PermissionSet? publish, PermissionSet? subscribe)
|
||||||
{
|
{
|
||||||
@@ -34,7 +33,12 @@ public sealed class ClientPermissions : IDisposable
|
|||||||
if (_publish == null)
|
if (_publish == null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return _pubCache.GetOrAdd(subject, _publish.IsAllowed);
|
if (_pubCache.TryGet(subject, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var allowed = _publish.IsAllowed(subject);
|
||||||
|
_pubCache.Set(subject, allowed);
|
||||||
|
return allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSubscribeAllowed(string subject, string? queue = null)
|
public bool IsSubscribeAllowed(string subject, string? queue = null)
|
||||||
|
|||||||
62
src/NATS.Server/Auth/PermissionLruCache.cs
Normal file
62
src/NATS.Server/Auth/PermissionLruCache.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
namespace NATS.Server.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fixed-capacity LRU cache for permission results.
|
||||||
|
/// Lock-protected (per-client, low contention).
|
||||||
|
/// Reference: Go client.go maxPermCacheSize=128.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PermissionLruCache
|
||||||
|
{
|
||||||
|
private readonly int _capacity;
|
||||||
|
private readonly Dictionary<string, LinkedListNode<(string Key, bool Value)>> _map;
|
||||||
|
private readonly LinkedList<(string Key, bool Value)> _list = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
public PermissionLruCache(int capacity = 128)
|
||||||
|
{
|
||||||
|
_capacity = capacity;
|
||||||
|
_map = new Dictionary<string, LinkedListNode<(string Key, bool Value)>>(capacity, StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(string key, out bool value)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_map.TryGetValue(key, out var node))
|
||||||
|
{
|
||||||
|
value = node.Value.Value;
|
||||||
|
_list.Remove(node);
|
||||||
|
_list.AddFirst(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string key, bool value)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_map.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
_list.Remove(existing);
|
||||||
|
existing.Value = (key, 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)>((key, value));
|
||||||
|
_list.AddFirst(node);
|
||||||
|
_map[key] = node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
tests/NATS.Server.Tests/PermissionLruCacheTests.cs
Normal file
52
tests/NATS.Server.Tests/PermissionLruCacheTests.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using NATS.Server.Auth;
|
||||||
|
|
||||||
|
namespace NATS.Server.Tests;
|
||||||
|
|
||||||
|
public class PermissionLruCacheTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Get_returns_none_for_unknown_key()
|
||||||
|
{
|
||||||
|
var cache = new PermissionLruCache(128);
|
||||||
|
cache.TryGet("foo", out _).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Set_and_get_returns_value()
|
||||||
|
{
|
||||||
|
var cache = new PermissionLruCache(128);
|
||||||
|
cache.Set("foo", true);
|
||||||
|
cache.TryGet("foo", out var v).ShouldBeTrue();
|
||||||
|
v.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Evicts_oldest_when_full()
|
||||||
|
{
|
||||||
|
var cache = new PermissionLruCache(3);
|
||||||
|
cache.Set("a", true);
|
||||||
|
cache.Set("b", true);
|
||||||
|
cache.Set("c", true);
|
||||||
|
cache.Set("d", true); // evicts "a"
|
||||||
|
|
||||||
|
cache.TryGet("a", out _).ShouldBeFalse();
|
||||||
|
cache.TryGet("b", out _).ShouldBeTrue();
|
||||||
|
cache.TryGet("d", out _).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Get_promotes_to_front()
|
||||||
|
{
|
||||||
|
var cache = new PermissionLruCache(3);
|
||||||
|
cache.Set("a", true);
|
||||||
|
cache.Set("b", true);
|
||||||
|
cache.Set("c", true);
|
||||||
|
|
||||||
|
// Access "a" to promote it
|
||||||
|
cache.TryGet("a", out _);
|
||||||
|
|
||||||
|
cache.Set("d", true); // should evict "b" (oldest untouched)
|
||||||
|
cache.TryGet("a", out _).ShouldBeTrue();
|
||||||
|
cache.TryGet("b", out _).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user