fix(acl): a stale-generation straggler can no longer roll the cache stamp backward and thrash ClearAll

This commit is contained in:
Joseph Doherty
2026-08-17 05:51:24 -04:00
parent f797df5187
commit 98d6a1908e
2 changed files with 74 additions and 2 deletions
@@ -263,6 +263,48 @@ public sealed class SessionAclDecisionCacheTests
}
}
/// <summary>
/// A request presenting an OLDER generation — one whose authority snapshot was captured before a
/// deploy and which then lost the scheduling race — must not roll the cache stamp backward, and must
/// not purge the new generation's entries on its way past.
/// </summary>
/// <remarks>
/// <para>
/// The generation itself is monotonic by construction; what is not monotonic is the order
/// requests <i>arrive</i> holding it. Treating a straggler as a deploy inverts the stamp and
/// wipes shards that were populated correctly, so the post-deploy window — the coldest the cache
/// ever is, and the busiest repopulating — is precisely when it would thrash.
/// </para>
/// <para>
/// Nothing here uses <c>AdvanceGenerationForTest</c>: this is the ordinary lookup path,
/// observed. The straggler must miss (its snapshot is superseded, so recomputing is right) AND
/// leave everything else standing — a test asserting only the miss passes against the defect,
/// because the straggler misses either way.
/// </para>
/// </remarks>
[Fact]
public void A_straggler_from_an_older_generation_neither_clears_nor_rolls_the_stamp_back()
{
var cache = new SessionAclDecisionCache();
Ask(cache, generation: 2, compute: () => true).ShouldBeTrue();
cache.Generation.ShouldBe(2);
cache.Count.ShouldBe(1);
// The straggler: same question, superseded snapshot.
cache.TryGet(generation: 1, Session, OpcUaOperation.Read, AddressSpaceRealm.Raw, Node, out _)
.ShouldBeFalse("a superseded snapshot must recompute rather than read the newer generation");
// Asserted BEFORE the generation-2 lookup below, which would itself restore a rolled-back stamp and
// hide the defect.
cache.Generation.ShouldBe(2, "the straggler must not have rolled the stamp backward");
cache.Count.ShouldBe(1, "and must not have purged the current generation's entries");
cache.TryGet(generation: 2, Session, OpcUaOperation.Read, AddressSpaceRealm.Raw, Node, out var decision)
.ShouldBeTrue("the current generation's decision survived the straggler");
decision.ShouldBeTrue();
}
/// <summary>
/// The store half of the same window: a <c>Set</c> that meets the previous generation's shard must
/// displace it, not fill it.