diff --git a/src/GameCult.Caching/Contracts/cultcache-store-composition.md b/src/GameCult.Caching/Contracts/cultcache-store-composition.md index d2c3bbeb..2d9894ab 100644 --- a/src/GameCult.Caching/Contracts/cultcache-store-composition.md +++ b/src/GameCult.Caching/Contracts/cultcache-store-composition.md @@ -144,11 +144,14 @@ which route wrote it. that cache's gate as its own lock, so a store's load callback into the cache 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, so under the cache's - gate `Cache` naming that cache cannot change, and under the store's gate it - cannot change at all. A call whose guess went stale (it queued on a cache that - then failed to attach, or on a store that then gained a cache) releases and - retries; a cacheless write that finds a cache is refused in that same hold. A + `Cache` is written only by the attach, holding both gates, once and after its + pull has succeeded (a failed attach never shows it, and nothing detaches), so + under the cache's gate `Cache` naming that cache cannot change, and under the + store's gate it cannot change at all. The pull reads the registry and the codec + the attach lent the store, not `Cache`. A call whose guess went stale (it + guessed no cache and the store then gained one) releases and retries once, so a + call makes at most two passes; a cacheless write that finds a cache is refused + in that same hold. 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 diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs index 1b6b2a91..3840c42c 100644 --- a/src/GameCult.Caching/CultCache.cs +++ b/src/GameCult.Caching/CultCache.cs @@ -2261,9 +2261,10 @@ namespace GameCult.Caching { if (store == null) throw new ArgumentNullException(nameof(store)); homes ??= Array.Empty(); - // The only writer of store.Cache, and it writes it only while holding the cache's gate and the store's, the failure path that - // resets Cache included: under the cache's gate Cache == this cache cannot change, and under the store's gate Cache cannot - // change at all. A store call that guessed wrong waits on whichever gate it took, finds the answer changed and retries. + // The only writer of store.Cache, and it writes it once, last, holding the cache's gate and the store's, after the pull has + // succeeded: a failed attach never publishes it, and there is no detach, so under the cache's gate Cache == this cache cannot + // change, and under the store's gate Cache cannot change at all. A store call that guessed null waits on the store's gate, finds + // the cache published and retries once. Held(() => store.UnderStoreGate(() => { if (store.Cache != null)