perf(acl): per-session decision-cache shards — a crawl no longer evicts the fleet
This commit is contained in:
@@ -111,20 +111,58 @@ public sealed class SessionAclDecisionCacheTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The cache is bounded. A client walking a huge address space must not be able to grow it without
|
||||
/// limit; clearing at the ceiling degrades to recomputation, which is the no-cache steady state.
|
||||
/// The global bound still holds when no single session is anywhere near its own ceiling. Spreading
|
||||
/// the entries over many sessions is the case the per-session cap cannot catch, so the global
|
||||
/// counter is the only thing standing between a large fleet and unbounded growth.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void The_cache_is_bounded()
|
||||
public void The_global_bound_still_holds()
|
||||
{
|
||||
var cache = new SessionAclDecisionCache();
|
||||
|
||||
for (var i = 0; i <= SessionAclDecisionCache.MaxEntries + 10; i++)
|
||||
Ask(cache, generation: 1, node: $"Plant/dev1/Tag{i}", compute: () => true);
|
||||
// Well under MaxEntriesPerSession, so every clear observed here is the global one.
|
||||
const int perSession = 5_000;
|
||||
var total = SessionAclDecisionCache.MaxEntries + perSession;
|
||||
|
||||
for (var i = 0; i < total; i++)
|
||||
Ask(
|
||||
cache, generation: 1, session: $"key-{i / perSession}", node: $"Plant/dev1/Tag{i}",
|
||||
compute: () => true);
|
||||
|
||||
// The global counter is approximate under concurrency, so the contract allows one shard's worth of
|
||||
// slack; single-threaded it is exact, which is the stronger fact pinned here.
|
||||
cache.Count.ShouldBeLessThanOrEqualTo(SessionAclDecisionCache.MaxEntries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One session filling its own shard evicts only itself. This is the whole point of sharding: a
|
||||
/// machine consumer crawling a 50k-node tree used to blow away every other session's decisions on
|
||||
/// its way past the ceiling, so the sessions doing ordinary work paid for the crawl.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_full_session_shard_clears_only_that_session()
|
||||
{
|
||||
var cache = new SessionAclDecisionCache();
|
||||
const string crawler = "crawler-key";
|
||||
const string neighbour = "neighbour-key";
|
||||
var neighbourCalls = 0;
|
||||
|
||||
Ask(cache, generation: 1, session: neighbour, compute: () => { neighbourCalls++; return true; })
|
||||
.ShouldBeTrue();
|
||||
neighbourCalls.ShouldBe(1);
|
||||
|
||||
for (var i = 0; i <= SessionAclDecisionCache.MaxEntriesPerSession + 10; i++)
|
||||
Ask(cache, generation: 1, session: crawler, node: $"Plant/dev1/Tag{i}", compute: () => true);
|
||||
|
||||
// The crawler's own shard was cleared and restarted rather than growing without limit.
|
||||
cache.Count.ShouldBeLessThanOrEqualTo(SessionAclDecisionCache.MaxEntriesPerSession);
|
||||
|
||||
// The neighbour's single decision survived the crawl — its walk is not re-run.
|
||||
Ask(cache, generation: 1, session: neighbour, compute: () => { neighbourCalls++; return true; })
|
||||
.ShouldBeTrue();
|
||||
neighbourCalls.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A decision taken while a deployment lands mid-walk is returned but NOT stored, so the new
|
||||
/// generation never inherits a verdict its own trie did not make.
|
||||
|
||||
Reference in New Issue
Block a user