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.
224 lines
6.9 KiB
C#
224 lines
6.9 KiB
C#
// Go reference: server/client.go routeCache, maxResultCacheSize=8192.
|
|
|
|
using NATS.Server.Subscriptions;
|
|
using Shouldly;
|
|
|
|
namespace NATS.Server.Tests.Subscriptions;
|
|
|
|
/// <summary>
|
|
/// Unit tests for RouteResultCache — the per-account LRU cache that stores
|
|
/// SubListResult objects keyed by subject string to avoid repeated SubList.Match() calls.
|
|
/// Go reference: server/client.go routeCache field, maxResultCacheSize=8192.
|
|
/// </summary>
|
|
public class RouteResultCacheTests
|
|
{
|
|
// -------------------------------------------------------------------------
|
|
// Helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
private static SubListResult MakeResult(string subject)
|
|
{
|
|
var sub = new Subscription { Subject = subject, Sid = "1" };
|
|
return new SubListResult([sub], []);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Basic lookup behaviour
|
|
// =========================================================================
|
|
|
|
/// <summary>
|
|
/// An empty cache must return false for any subject.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TryGet_returns_false_on_empty_cache()
|
|
{
|
|
var cache = new RouteResultCache(capacity: 16);
|
|
|
|
var found = cache.TryGet("foo.bar", out var result);
|
|
|
|
found.ShouldBeFalse();
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A result placed with Set must be retrievable via TryGet.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Set_and_TryGet_round_trips_result()
|
|
{
|
|
var cache = new RouteResultCache(capacity: 16);
|
|
var expected = MakeResult("foo");
|
|
cache.Set("foo", expected);
|
|
|
|
var found = cache.TryGet("foo", out var actual);
|
|
|
|
found.ShouldBeTrue();
|
|
actual.ShouldBeSameAs(expected);
|
|
}
|
|
|
|
// =========================================================================
|
|
// LRU eviction
|
|
// =========================================================================
|
|
|
|
/// <summary>
|
|
/// When a capacity+1 entry is added, the oldest (LRU) entry must be evicted.
|
|
/// Go reference: maxResultCacheSize=8192 with LRU eviction.
|
|
/// </summary>
|
|
[Fact]
|
|
public void LRU_eviction_at_capacity()
|
|
{
|
|
const int cap = 4;
|
|
var cache = new RouteResultCache(capacity: cap);
|
|
|
|
// Fill to capacity — "first" is the oldest (LRU)
|
|
cache.Set("first", MakeResult("first"));
|
|
cache.Set("second", MakeResult("second"));
|
|
cache.Set("third", MakeResult("third"));
|
|
cache.Set("fourth", MakeResult("fourth"));
|
|
|
|
// Adding a 5th entry must evict "first"
|
|
cache.Set("fifth", MakeResult("fifth"));
|
|
|
|
cache.Count.ShouldBe(cap);
|
|
cache.TryGet("first", out _).ShouldBeFalse();
|
|
cache.TryGet("fifth", out _).ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// After accessing A, B, C (capacity=3), accessing A promotes it to MRU.
|
|
/// The next Set evicts B (now the true LRU) rather than A.
|
|
/// </summary>
|
|
[Fact]
|
|
public void LRU_order_updated_on_access()
|
|
{
|
|
var cache = new RouteResultCache(capacity: 3);
|
|
cache.Set("A", MakeResult("A"));
|
|
cache.Set("B", MakeResult("B"));
|
|
cache.Set("C", MakeResult("C"));
|
|
|
|
// Access A so it becomes MRU; now LRU order is: B, A, C (front=C, back=B)
|
|
cache.TryGet("A", out _);
|
|
|
|
// D evicts the LRU entry — should be B, not A
|
|
cache.Set("D", MakeResult("D"));
|
|
|
|
cache.TryGet("A", out _).ShouldBeTrue();
|
|
cache.TryGet("B", out _).ShouldBeFalse();
|
|
cache.TryGet("C", out _).ShouldBeTrue();
|
|
cache.TryGet("D", out _).ShouldBeTrue();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Generation / invalidation
|
|
// =========================================================================
|
|
|
|
/// <summary>
|
|
/// After Invalidate(), the next TryGet must detect the generation mismatch,
|
|
/// clear all entries, and return false.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Invalidate_clears_on_next_access()
|
|
{
|
|
var cache = new RouteResultCache(capacity: 16);
|
|
cache.Set("a", MakeResult("a"));
|
|
cache.Set("b", MakeResult("b"));
|
|
|
|
cache.Invalidate();
|
|
|
|
cache.TryGet("a", out var result).ShouldBeFalse();
|
|
result.ShouldBeNull();
|
|
cache.Count.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Each call to Invalidate() must increment the generation counter by 1.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Generation_increments_on_invalidate()
|
|
{
|
|
var cache = new RouteResultCache();
|
|
var gen0 = cache.Generation;
|
|
|
|
cache.Invalidate();
|
|
cache.Generation.ShouldBe(gen0 + 1);
|
|
|
|
cache.Invalidate();
|
|
cache.Generation.ShouldBe(gen0 + 2);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Statistics
|
|
// =========================================================================
|
|
|
|
/// <summary>
|
|
/// Hits counter increments on each successful TryGet.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Hits_incremented_on_cache_hit()
|
|
{
|
|
var cache = new RouteResultCache(capacity: 16);
|
|
cache.Set("subject", MakeResult("subject"));
|
|
|
|
cache.TryGet("subject", out _);
|
|
cache.TryGet("subject", out _);
|
|
|
|
cache.Hits.ShouldBe(2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Misses counter increments on each failed TryGet.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Misses_incremented_on_cache_miss()
|
|
{
|
|
var cache = new RouteResultCache(capacity: 16);
|
|
|
|
cache.TryGet("nope", out _);
|
|
cache.TryGet("also.nope", out _);
|
|
|
|
cache.Misses.ShouldBe(2);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Clear
|
|
// =========================================================================
|
|
|
|
/// <summary>
|
|
/// Clear() must remove all entries immediately.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Clear_removes_all_entries()
|
|
{
|
|
var cache = new RouteResultCache(capacity: 16);
|
|
cache.Set("x", MakeResult("x"));
|
|
cache.Set("y", MakeResult("y"));
|
|
|
|
cache.Clear();
|
|
|
|
cache.Count.ShouldBe(0);
|
|
cache.TryGet("x", out _).ShouldBeFalse();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Update existing entry
|
|
// =========================================================================
|
|
|
|
/// <summary>
|
|
/// Setting the same key twice must replace the value; TryGet returns the latest result.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Set_updates_existing_entry()
|
|
{
|
|
var cache = new RouteResultCache(capacity: 16);
|
|
var first = MakeResult("v1");
|
|
var second = MakeResult("v2");
|
|
|
|
cache.Set("key", first);
|
|
cache.Set("key", second);
|
|
|
|
cache.Count.ShouldBe(1);
|
|
cache.TryGet("key", out var returned).ShouldBeTrue();
|
|
returned.ShouldBeSameAs(second);
|
|
}
|
|
}
|