Tighten the gate-bound tests: two gates under failing churn, exactly one when detached or settled, three reached under successful churn

Not built or run at this commit: verified on Yggdrasil at the cut's head.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-27 03:39:02 +01:00
commit 5036244778
2 changed files with 70 additions and 5 deletions

View file

@ -3617,7 +3617,8 @@ namespace GameCult.Caching
internal readonly object _storeGate = new();
// Gates the current thread has taken for store calls, read by tests (InternalsVisibleTo) to pin the bound of three per call.
// Gates the current thread has taken for store calls, read by tests (InternalsVisibleTo) to pin the two bounds of Gate<T>: at
// most two under failing-attach churn (Cache read under the store's gate is then always null), at most three otherwise.
[ThreadStatic]
internal static int GateTakes;

View file

@ -1420,10 +1420,74 @@ namespace GameCult.Caching.Tests
c.Dispose();
}
// One call takes at most three gates (the guessed cache's, the store's, the settled cache's), however often failing attaches
// publish and retract Cache under it. A retry around the unlocked guess would take one per stale guess and exceed this.
// A call on a detached store takes exactly one gate (the store's), and one on a store whose attach has settled takes exactly
// one (the settled cache's, reached by the guess): the counter counts each take.
[Test]
public void OneStoreCallTakesAtMostThreeGatesUnderFailingAttachChurn()
public void OneStoreCallTakesExactlyOneGateOnADetachedStoreAndOnASettledOne()
{
var registry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) });
new SingleFileMessagePackBackingStore(_path).WriteDocument(new CultRecordKey("a"), new IdDeck { Name = "a" }, registry: registry);
var store = new RoleStore(_path);
store.ReleaseAttach.Set();
CacheBackingStore.GateTakes = 0;
store.ReadStoreSnapshot();
Assert.That(CacheBackingStore.GateTakes, Is.EqualTo(1), "a detached store's call takes the store's gate");
using var cache = new CultCache(registry);
cache.AddBackingStore(store);
CacheBackingStore.GateTakes = 0;
store.ReadStoreSnapshot();
Assert.That(CacheBackingStore.GateTakes, Is.EqualTo(1), "a settled store's call takes only its cache's gate");
}
// Under attaches that succeed, a call takes at most three gates, and three is reachable: the guess was null, an attach then
// settled before the call took the store's gate, and the call took the settled cache's gate as its third.
[Test]
public void OneStoreCallTakesAtMostThreeGatesUnderSuccessfulAttachChurnAndThreeIsReached()
{
var registry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) });
new SingleFileMessagePackBackingStore(_path).WriteDocument(new CultRecordKey("a"), new IdDeck { Name = "a" }, registry: registry);
var mostTakes = 0;
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(60);
for (var round = 0; mostTakes < 3 && DateTime.UtcNow < deadline; round++)
{
var store = new RoleStore(_path);
store.ReleaseAttach.Set();
var cache = new CultCache(registry);
using var barrier = new Barrier(2);
var takes = 0;
var spin = (round % 16) * 40;
var caller = new Thread(() =>
{
barrier.SignalAndWait();
CacheBackingStore.GateTakes = 0;
store.ReadStoreSnapshot();
takes = CacheBackingStore.GateTakes;
});
var attacher = new Thread(() =>
{
barrier.SignalAndWait();
Thread.SpinWait(spin);
cache.AddBackingStore(store);
});
caller.Start();
attacher.Start();
Finishes(caller, "a calling thread");
Finishes(attacher, "an attaching thread");
cache.Dispose();
Assert.That(takes, Is.InRange(1, 3), "no call took more than three gates");
mostTakes = Math.Max(mostTakes, takes);
}
Assert.That(mostTakes, Is.EqualTo(3), "a call that raced a successful attach took the third gate");
}
// Under attaches that fail, Cache read under the store's gate is always null (an attach publishes and retracts it inside one
// hold of that gate), so one call takes at most two gates: the guessed cache's, then the store's. A retry around the unlocked
// guess would take one per stale guess and exceed this.
[Test]
public void OneStoreCallTakesAtMostTwoGatesUnderFailingAttachChurn()
{
var registry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) });
new SingleFileMessagePackBackingStore(_path).WriteDocument(new CultRecordKey("a"), new IdDeck { Name = "a" }, registry: registry);
@ -1459,7 +1523,7 @@ namespace GameCult.Caching.Tests
Finishes(t.Thread, "a churn thread");
Assert.That(calls, Is.GreaterThan(100), "the readers made calls under churn");
Assert.That(mostTakes, Is.LessThanOrEqualTo(3), "no call took more than three gates");
Assert.That(mostTakes, Is.LessThanOrEqualTo(2), "no call took more than two gates");
foreach (var c in caches)
c.Dispose();
}