fix(core): resolve Medium code-review finding (Core-005)
Change ClusterEntry from sealed record to sealed class so TryUpdate uses reference equality for the CAS comparison. Prune now uses a read-compute-TryUpdate retry loop that restarts when a concurrent Install updates the entry between the read and the write, preventing a race that could silently drop the just-installed newest generation. Two regression tests added to PermissionTrieCacheTests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,4 +101,39 @@ public sealed class PermissionTrieCacheTests
|
||||
cache.CurrentGenerationId("c1").ShouldBe(1);
|
||||
cache.CurrentGenerationId("c2").ShouldBe(9);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core-005 regression: a concurrent Install that adds a new generation between a Prune
|
||||
/// read and write must not be silently overwritten. We can't deterministically reproduce
|
||||
/// a true data race in a unit test, but we can verify that Prune performed after Install
|
||||
/// honours the newly-installed generation rather than clobbering it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Prune_AfterConcurrentInstall_PreservesLatestGeneration()
|
||||
{
|
||||
var cache = new PermissionTrieCache();
|
||||
for (var g = 1L; g <= 5; g++) cache.Install(Trie("c1", g));
|
||||
|
||||
// Simulate: Install of generation 6 races with a pending Prune(keepLatest:2).
|
||||
// After Prune runs, generation 6 (the "just installed" one) must still be current.
|
||||
cache.Install(Trie("c1", 6));
|
||||
cache.Prune("c1", keepLatest: 2);
|
||||
|
||||
cache.CurrentGenerationId("c1").ShouldBe(6, "current must remain the newest installed generation");
|
||||
cache.GetTrie("c1", 6).ShouldNotBeNull("generation 6 must be retained as current");
|
||||
cache.GetTrie("c1", 5).ShouldNotBeNull("generation 5 retained (keepLatest=2)");
|
||||
cache.GetTrie("c1", 4).ShouldBeNull("generation 4 was pruned");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prune_Current_Pointer_Survives_Pruning()
|
||||
{
|
||||
// Current is always the highest generation; verify it is not lost after prune.
|
||||
var cache = new PermissionTrieCache();
|
||||
for (var g = 1L; g <= 6; g++) cache.Install(Trie("c1", g));
|
||||
|
||||
cache.Prune("c1", keepLatest: 3);
|
||||
|
||||
cache.GetTrie("c1")!.GenerationId.ShouldBe(6, "current must still be the highest generation after pruning");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user