diff --git a/src/GameCult.Caching/Contracts/cultcache-store-composition.md b/src/GameCult.Caching/Contracts/cultcache-store-composition.md index 1400025f..c25a36c4 100644 --- a/src/GameCult.Caching/Contracts/cultcache-store-composition.md +++ b/src/GameCult.Caching/Contracts/cultcache-store-composition.md @@ -145,16 +145,16 @@ 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, 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 the attach - lent, not `Cache`; a variant it loads carries no codec, and the cache resolves - it with its own. Until `Cache` is published the store's hold is the attach's, - so a journal the pull runs cannot write the store cacheless. 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 + `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 + 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 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/unity/org.gamecult.cultlib/CHANGELOG.md b/unity/org.gamecult.cultlib/CHANGELOG.md index 88160f4f..c9530cbb 100644 --- a/unity/org.gamecult.cultlib/CHANGELOG.md +++ b/unity/org.gamecult.cultlib/CHANGELOG.md @@ -23,7 +23,7 @@ Releases as 3.0.0 (major). ### Added -- A cacheless `WriteRecord` or `WriteDocument`, and an attach, hold the store's gate while they run, and user code they run (a document's property getters, a constructor while a copied record is read) now throws `InvalidOperationException` if it enters a cache it does not hold or a second store; it used to wait forever against an attach of the same store. A store call that queued on an attach that then failed runs under the gate that owns the store, not beside a cacheless call or a second cache's attach. An attach that fails takes its registry back, so a `PullAll` on the store afterwards is refused as unattached instead of loading records the next cache never sees. A cacheless `WriteRecord` or `WriteDocument` made while the same thread already holds that store (from a journal an attach's pull runs, or from user code another cacheless write of the store runs) throws `InvalidOperationException`; it used to write the file behind the attaching cache and take that cache's registry, so the cache could never flush or pull the store again. +- A cacheless `WriteRecord` or `WriteDocument`, and an attach, hold the store's gate while they run, and user code they run (a document's property getters, a constructor while a copied record is read) now throws `InvalidOperationException` if it enters a cache it does not hold or a second store; it used to wait forever against an attach of the same store. A store call that queued on an attach that then failed runs under the gate that owns the store, not beside a cacheless call or a second cache's attach. An attach that fails takes its registry back, so a `PullAll` on the store afterwards is refused as unattached instead of loading records the next cache never sees. A cacheless `WriteRecord` or `WriteDocument` made while the same thread already holds that store (from a journal an attach's pull runs, or from user code another cacheless write of the store runs) throws `InvalidOperationException`; it used to write the file behind the attaching cache and take that cache's registry, so the cache could never flush or pull the store again. A journal that calls its cache's store (`Push`, `Delete`, `PushAll`, `CommitBatch`, `PullAll`, `ReadStoreSnapshot`, `JudgeChecks`) while that cache's `AddBackingStore` runs the journal now throws "A journal entered the cache it journals", as it does after the attach; it used to stage or write records the cache never admitted, so the cache and the file disagreed. - `SingleFileBackingStore.ReadStoreSnapshot()` reads a store file once, under the store's in-process gate, with the reader a cache's store opens with, and returns the durable snapshot or null when nothing is at the path. `SingleFileBackingStore.WriteRecord(record, catalogEntry, holdsIds, registry)` writes one record without a cache: under the store's lock every other record and catalog entry is copied as stored, the file is replaced atomically, and the header is decided as a flush decides it. It refuses on a store attached to a cache. - `SingleFileBackingStore.WriteDocument(key, document, storedAt, registry)` writes one typed document without a cache, as a cache write does: it plans element ids, encodes the document as `T` (the type its schema id and ids are taken from, also for a derived instance) and marks the store when it holds an id; a failed write gives the minted ids back. It refuses on a store attached to a cache. The `registry` of `WriteRecord` and `WriteDocument` serves that call only; a store keeps none. - `CultCodec.DecodeDocument(key, payload)` decodes a stored payload as a cache load does (`CultCacheMessagePack.CreateCodec(registry)` makes the codec). It reads no file and holds no state. `CultElementIds` stays internal.