Caching docs: the lock order is cache gate, store gate, file lock; the gate owner re-checks after locking (contracts, CHANGELOG)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1eb517296b
commit
3c5f1fe0c2
3 changed files with 24 additions and 6 deletions
|
|
@ -109,7 +109,8 @@ parity cuts land.
|
|||
A store that fails to hydrate on open (corrupt bytes, unresolvable schema)
|
||||
makes the open throw and is left byte-identical; it is never silently
|
||||
overwritten. Within one process the lock order is the cache's gate, then the
|
||||
store's file lock; a store's I/O blocks its cache's readers.
|
||||
store's gate, then the file lock; nothing under a store's gate enters a cache's
|
||||
gate. A store's I/O blocks its cache's readers.
|
||||
|
||||
C# implements batch and conditional commit (`CultCache.Commit`, `TryCommit`,
|
||||
`CultCacheBatch`). Rust implements both. TypeScript and Python implement
|
||||
|
|
|
|||
|
|
@ -126,10 +126,26 @@ which route wrote it.
|
|||
|
||||
## Locking
|
||||
|
||||
- One lock order: the cache's gate, then the store's lock. A store attached to a
|
||||
cache takes 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 store must not call `Loaded`
|
||||
while holding its own lock outside the cache's hold.
|
||||
- One lock order: the cache's gate, then the store's gate, then the file lock. A
|
||||
thread holds at most one cache's gate and at most one store's gate. A store's
|
||||
gate may be taken inside a cache's gate (the attach; a journal writing a
|
||||
cacheless file), never the reverse: while a thread holds a store's gate it
|
||||
enters no cache gate it does not already hold, reads included, and no other
|
||||
store's gate. Breaking the order throws `InvalidOperationException`. This is
|
||||
checked at run time, not by comment: user code a cacheless call or an attach
|
||||
runs under the store's gate (a document's getter, a constructor while a copied
|
||||
record is read) is the way in.
|
||||
- One owner picks the gate a store call holds. A store attached to a cache takes
|
||||
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
|
||||
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
|
||||
that cache. It must not enter the cache, and must not wait for anything that
|
||||
needs the cache or its gate: another thread's write, a lock a writer holds.
|
||||
|
|
@ -354,7 +370,7 @@ example `SingleFileMessagePackBackingStore`): `ReadStoreSnapshot()`, `WriteRecor
|
|||
and `WriteDocument<T>(key, document, storedAt, registry)`. Decoding a stored payload needs no file, so it is on the codec, not the store:
|
||||
`CultCodec.DecodeDocument<T>(key, payload)` (`CultCacheMessagePack.CreateCodec(registry)` makes the codec).
|
||||
|
||||
- `ReadStoreSnapshot()` reads the file once, under the store's gate, and returns the durable snapshot (records, schema catalog,
|
||||
- `ReadStoreSnapshot()` reads the file once, under the store's gate (or its cache's gate when attached, validated after it is taken), and returns the durable snapshot (records, schema catalog,
|
||||
format stamp), or null when nothing is at the path. It throws `CultStoreUnreadableException` for a file the reader refuses.
|
||||
- `WriteRecord` writes one record under the store's lock: the record replaces the one at its key, every other record and catalog
|
||||
entry is copied forward as stored, and the file is replaced atomically. It takes a payload the caller already encoded, so a
|
||||
|
|
|
|||
|
|
@ -23,6 +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.
|
||||
- `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<T>(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<T>(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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue