Store gate: Cache owns the attach window; pin the three-gate bound; fix the unseen-cache wording
A cacheless write that re-enters its own store's hold now consults Cache first, so a journal's WriteDocument or WriteRecord gets the same refusal during an attach as after it (the attached text); the re-entry text no longer names an attach's pull. InStoreHold takes the store instead of its gate. CacheBackingStore.GateTakes (internal, thread-static) counts the gates Gate takes; OneStoreCallTakesAtMostThreeGatesUnderFailingAttachChurn asserts at most three per call under failing-attach churn, which pins the stale-guess loop (M4b). The contract and AddBackingStore say what holds: other threads may guess the tentative cache, but none runs a body against it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
74021e2326
commit
193cf1c39e
4 changed files with 75 additions and 13 deletions
|
|
@ -1186,6 +1186,8 @@ namespace GameCult.Caching.Tests
|
|||
|
||||
private const string ReentryRefusal = "A cacheless write was made inside a call that already holds this store";
|
||||
|
||||
private const string AttachedRefusal = "The store is attached to a cache, write through the cache.";
|
||||
|
||||
private const string JournalRefusal = "A journal entered the cache it journals";
|
||||
|
||||
// Every store entry point, called by a journal the attach's pull runs on the attaching thread, through a real attach. The store is
|
||||
|
|
@ -1276,7 +1278,7 @@ namespace GameCult.Caching.Tests
|
|||
|
||||
Assert.That(journalRuns, Is.GreaterThan(0), "the pull's admission ran the journal");
|
||||
Assert.That(journalError, Is.InstanceOf<InvalidOperationException>());
|
||||
Assert.That(journalError!.Message, Does.StartWith(op is "writedoc" or "writerecord" ? ReentryRefusal : JournalRefusal));
|
||||
Assert.That(journalError!.Message, Does.StartWith(op is "writedoc" or "writerecord" ? AttachedRefusal : JournalRefusal));
|
||||
Assert.That(store.IsDirty, Is.False, "nothing the cache never admitted is staged");
|
||||
|
||||
await cache.UpsertAsync(typeof(IdDeck), new IdDeck { Name = "later" }, new CultRecordKey("later"));
|
||||
|
|
@ -1417,5 +1419,49 @@ namespace GameCult.Caching.Tests
|
|||
foreach (var c in caches)
|
||||
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.
|
||||
[Test]
|
||||
public void OneStoreCallTakesAtMostThreeGatesUnderFailingAttachChurn()
|
||||
{
|
||||
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) { FailAttachPull = true };
|
||||
store.ReleaseAttach.Set();
|
||||
var caches = Enumerable.Range(0, 4).Select(_ => new CultCache(registry)).ToArray();
|
||||
var stop = 0;
|
||||
var mostTakes = 0;
|
||||
var calls = 0;
|
||||
var threads = caches.Select(c => OnThread("attach", () =>
|
||||
{
|
||||
while (Volatile.Read(ref stop) == 0)
|
||||
{
|
||||
try { c.AddBackingStore(store); }
|
||||
catch (InvalidOperationException e) when (e.Message.Contains("pull fails")) { }
|
||||
}
|
||||
})).Concat(Enumerable.Range(0, 4).Select(_ => OnThread("counting-reader", () =>
|
||||
{
|
||||
while (Volatile.Read(ref stop) == 0)
|
||||
{
|
||||
CacheBackingStore.GateTakes = 0;
|
||||
try { store.ReadStoreSnapshot(); }
|
||||
catch (InvalidOperationException) { }
|
||||
var takes = CacheBackingStore.GateTakes;
|
||||
Interlocked.Increment(ref calls);
|
||||
int seen;
|
||||
while (takes > (seen = Volatile.Read(ref mostTakes)) && Interlocked.CompareExchange(ref mostTakes, takes, seen) != seen) { }
|
||||
}
|
||||
}))).ToArray();
|
||||
Thread.Sleep(TimeSpan.FromSeconds(3));
|
||||
Volatile.Write(ref stop, 1);
|
||||
foreach (var t in threads)
|
||||
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");
|
||||
foreach (var c in caches)
|
||||
c.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue