From edb6703d3b8f19ccdc82580dfd4f03a9ada8a6fc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 13 Sep 2026 18:32:58 +0200 Subject: [PATCH] Record Cut 3's concurrency and commit decisions Operator chose option A on all three questions Soul raised: one lock order (cache, then store) with store I/O blocking that cache's readers; an unconditional Commit is last-writer-wins like a flush; a store that fails to hydrate on open throws and is never overwritten. Also records one key per store, load-only OnUpdate, missing-file dirtiness, the late-route fixture, TryGet kept for AquaSynth, the scoped SemaphoreSlim check, and the corrected size target for CultCache.cs. Co-Authored-By: Claude Opus 5 --- docs/cultcache-migration-cut.md | 34 ++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/cultcache-migration-cut.md b/docs/cultcache-migration-cut.md index 0e76d98c4..23eb5b96d 100644 --- a/docs/cultcache-migration-cut.md +++ b/docs/cultcache-migration-cut.md @@ -333,7 +333,8 @@ the two Caching projects. | `MaterializeMissingGlobals`, `CultCache(registry, bool)` | `CultCacheMessagePack.cs:105` only | delete (2.1) | | `CultCacheOpenOptions.ConfigureCache`, `ConfigureStore`, `DirectoryStorePath`, `ConfigureDirectoryStore`, `DirectoryStoreHydrationFilter` | none (`DirectoryStoreHydrationFilter` tests only) | delete | | `FlushBackingStore(store)`, `PrepareForReloadOrShutdown[Async]` | none | delete; `FlushAsync` writes every dirty store | -| `TryGet<`, `TryGetByName`, `TryGetByIndex`, `Resolve<` | none | delete | +| `TryGet<` | AquaSynth (`CultCachePatchDocument.cs:104`), missed by the first audit | keep | +| `TryGetByName`, `TryGetByIndex`, `Resolve<` | none | delete | | `CultCache.Logger`, store `Logger`, `NullLogger` use | none | delete | | store `FlushOnDispose` | none directly; `CultCacheOpenOptions.StoreFlushOnDispose` (Gjallar, Ymir, Delvehold) sets it | keep both; collapsing the two flush-on-dispose flags is a follow-up | | store `HydrationFilter`, `PullSelected`, `PullBackingStoreRecordsAsync`, `CultPersistedRecordMetadata` | tests only | delete | @@ -363,7 +364,11 @@ the two Caching projects. **Target shape after Cuts 2-4** (the original `Aetheria\...\CultCache.cs` is 437 lines for a cache, routing, an inheritance-aware index and six stores; -this is what `CultCache.cs` should read like at about 900 lines including the +this is what the cache half of `CultCache.cs` should read like; the "about 900 +lines including the registry" figure was wrong: the registry and schema half +alone is 1,057 lines and is not reshaped by this migration (Cut 3 measured the +cache half at 1,025 lines after adding routing and commit). Original sketch, +at about 900 lines including the registry): ```csharp @@ -561,6 +566,29 @@ home store, before `_entries` changes. `PublishChange`'s `Activator.CreateInstance` (`CC:2286-2301`) goes. `[P2-D]` shows `Watch` receives nothing today. +**Concurrency and commit semantics (Soul, Cut 3; operator decisions 2026-09-13, all option A).** +- Lock order is the cache gate, then the store lock, on every path: attach, + pull, write, flush, commit. A store's I/O blocks that cache's readers; no + finer-grained locking. (Cut 3 as first landed took them in both orders and + deadlocked a directory-store pull against an upsert, and a re-pull could erase + a staged single-file write.) +- An unconditional `Commit` writes the cache's snapshot of its home store, + exactly like a flush: last-writer-wins. Only conditional commits (`Expect`, + `ExpectUnchanged`) protect against other writers. A `Commit` also persists + earlier staged writes to that store and clears dirty. +- One key lives in one store: a write or load that would put a key already held + from another store throws and changes nothing. +- `OnUpdate` fires for loads only, as before the migration; writers publish + their own changes. +- A missing single-file store keeps staged writes dirty and does not drop loaded + records. +- A store that fails to hydrate on open (corrupt, or an unregistered schema) + throws and is never overwritten; consumers such as Gjallar and AquaSynth's + writers surface the error rather than deleting and rewriting. +- `LateRouteOverAdmittedTypeThrows`: the reverse-order fixture holds only + unrouted types in the untyped store, since a routed type there is a foreign + record. + **Read-only stores.** `CacheBackingStore(bool readOnly = false)`, `IsReadOnly`; `SingleFileMessagePackBackingStore(string filePath, bool readOnly = false)`; `DirectoryMessagePackBackingStore(string manifestPath, @@ -972,7 +1000,7 @@ the behavior change. - Soul: every test; schema-compatibility fixture ids unchanged; Mesh, Networking, Geometry suites at their Cut 2 counts; external builds green; the AquaSynth global report written into this document's section 7; - `CC` at or below the 2.0 shape's size; `rg "MaterializeMissingGlobals|InitializeGlobals|ContainsDurableRecord|PullOnOpen|initializeGlobals|ExecuteTransactionAsync|AsyncLocal|SemaphoreSlim|VisibleStoredDocuments" src tests` empty. + `CC` at or below the 2.0 shape's size; `rg "MaterializeMissingGlobals|InitializeGlobals|ContainsDurableRecord|PullOnOpen|initializeGlobals|ExecuteTransactionAsync|VisibleStoredDocuments" src tests` empty, and `rg "AsyncLocal|SemaphoreSlim" src\GameCult.Caching` empty (Mesh and Networking use `SemaphoreSlim` for unrelated reasons). - Atomic commit, in the same commit as the routing change: delete `CC:1577-1622` (`ExecuteTransactionAsync` ×2), `:1907-1921` (`VisibleStoredDocuments`), `:1923-2066` (`CommitTransaction`,