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
|
|
@ -146,15 +146,17 @@ which route wrote it.
|
|||
cannot take the two out of order; a cacheless store takes its own gate. The
|
||||
store reads which one applies, takes it, and checks the answer again under it:
|
||||
`Cache` is written only by the attach, holding both gates: it publishes `Cache`
|
||||
before its pull and takes it back in the same hold if the pull fails, so no
|
||||
other thread ever sees the cache come and go. Under the store's gate `Cache` is
|
||||
before its pull and takes it back in the same hold if the pull fails. Another
|
||||
thread's unlocked guess may read the tentative cache, but none runs a body
|
||||
against it: the guess is checked under that cache's gate, which the attach
|
||||
holds, and fails once the pull retracts it. Under the store's gate `Cache` is
|
||||
settled, and a settled cache never changes, because nothing detaches. A call
|
||||
takes at most three gates: the guessed cache's, the store's, the settled
|
||||
cache's. The pull reads the registry the attach lent, not `Cache`; a variant it
|
||||
loads carries no codec, and the cache resolves it with its own. During an
|
||||
attach the store is attached, so a journal the pull runs that calls the store
|
||||
is refused as it would be afterwards; a cacheless write that finds a cache is
|
||||
refused in that same hold. A
|
||||
is refused as it would be afterwards; a cacheless write that finds a cache
|
||||
published, during an attach or after it, is refused with the same text. A
|
||||
store must not call `Loaded` while holding its own lock outside the cache's
|
||||
hold.
|
||||
- A journal runs under the cache's gate, so a slow journal slows every writer of
|
||||
|
|
|
|||
|
|
@ -2264,7 +2264,8 @@ namespace GameCult.Caching
|
|||
// The only writer of store.Cache, holding the cache's gate and the store's. It publishes Cache before the pull, so the store is
|
||||
// attached for the whole of its hold and every entry point agrees: a journal the pull runs that calls the store is a journal
|
||||
// entering its cache and is refused, as it is after the attach; every other thread waits on the cache's or the store's gate.
|
||||
// A failed pull takes Cache back in that same hold, so no other thread ever sees the cache come and go, and there is no detach.
|
||||
// A failed pull takes Cache back in that same hold. Another thread's unlocked guess may read the tentative cache, but none runs a
|
||||
// body against it: the guess is checked under that cache's gate, which the attach holds, and fails once the pull retracts it.
|
||||
Held(() => store.UnderStoreGate(() =>
|
||||
{
|
||||
if (store.Cache != null)
|
||||
|
|
@ -2824,15 +2825,19 @@ namespace GameCult.Caching
|
|||
|
||||
// The one lock of a store's gate. A thread holds one store's gate at a time; a store call may be entered again from inside it,
|
||||
// except a cacheless write: it owns its store outright, so it is the outermost hold of the store's gate.
|
||||
internal static T InStoreHold<T>(object storeGate, Func<T> body, bool cacheless = false)
|
||||
// A cacheless write that re-enters its own store's hold is refused by whichever fact is true: Cache is the one attachment fact, so
|
||||
// a store with a cache published (an attach's pull included) gets the attached refusal, and one without gets the re-entry refusal.
|
||||
internal static T InStoreHold<T>(CacheBackingStore store, Func<T> body, bool cacheless = false)
|
||||
{
|
||||
var storeGate = store._storeGate;
|
||||
if (_storeHeld != null)
|
||||
{
|
||||
if (ReferenceEquals(_storeHeld, storeGate))
|
||||
{
|
||||
if (cacheless)
|
||||
throw new InvalidOperationException(
|
||||
"A cacheless write was made inside a call that already holds this store (an attach's pull, or a store call whose user code made it); write through the cache.");
|
||||
throw new InvalidOperationException(store.Cache != null
|
||||
? CacheBackingStore.AttachedRefusal
|
||||
: "A cacheless write was made inside a call that already holds this store (a store call whose user code made it); write through the cache.");
|
||||
return body();
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -3586,7 +3591,13 @@ namespace GameCult.Caching
|
|||
// any of it; if the cache refuses a record the call throws and the store keeps its previous view.
|
||||
protected internal Action<IReadOnlyList<CultStoredDocument>, IReadOnlyList<CultStoredDocument>>? Loaded;
|
||||
|
||||
private readonly object _storeGate = new();
|
||||
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.
|
||||
[ThreadStatic]
|
||||
internal static int GateTakes;
|
||||
|
||||
internal const string AttachedRefusal = "The store is attached to a cache, write through the cache.";
|
||||
// Written only by CultCache.AddBackingStore, holding the cache's gate and this store's: set before the pull, and set back to null
|
||||
// in the same hold if the pull fails. Read under this store's gate it is settled, and a settled cache never changes, because
|
||||
// nothing detaches.
|
||||
|
|
@ -3647,23 +3658,26 @@ namespace GameCult.Caching
|
|||
var guess = cacheless ? null : Volatile.Read(ref Cache);
|
||||
if (guess != null)
|
||||
{
|
||||
GateTakes++;
|
||||
var (ran, result) = guess.Held(() => Cache == guess ? (true, body()) : (false, default(T)!));
|
||||
if (ran)
|
||||
return result;
|
||||
}
|
||||
|
||||
GateTakes++;
|
||||
var (done, value, settled) = UnderStoreGate(() =>
|
||||
{
|
||||
var cache = Cache;
|
||||
if (cache == null)
|
||||
return (true, body(), (CultCache?)null);
|
||||
if (cacheless)
|
||||
throw new InvalidOperationException("The store is attached to a cache, write through the cache.");
|
||||
throw new InvalidOperationException(AttachedRefusal);
|
||||
return (false, default(T)!, cache);
|
||||
}, cacheless);
|
||||
if (done)
|
||||
return value;
|
||||
|
||||
GateTakes++;
|
||||
return settled!.Held(() => Cache == settled
|
||||
? body()
|
||||
: throw new InvalidOperationException(
|
||||
|
|
@ -3692,7 +3706,7 @@ namespace GameCult.Caching
|
|||
}, cacheless: true);
|
||||
|
||||
// The store's own gate, taken only here. Lock order: a cache's gate, then a store's; nothing takes a store's gate and then a cache's.
|
||||
internal T UnderStoreGate<T>(Func<T> body, bool cacheless = false) => CultCache.InStoreHold(_storeGate, body, cacheless);
|
||||
internal T UnderStoreGate<T>(Func<T> body, bool cacheless = false) => CultCache.InStoreHold(this, body, cacheless);
|
||||
|
||||
// The one writer of the lent registry: a cacheless call for the call, an attach for as long as it holds the store, and a failed
|
||||
// attach puts it back, so a cacheless store holds none between calls and a pull on one loads nothing. Nothing else is lent, and the
|
||||
|
|
|
|||
Loading…
Reference in a new issue