diff --git a/src/GameCult.Caching/Contracts/cultcache-persistence-format.md b/src/GameCult.Caching/Contracts/cultcache-persistence-format.md index 9dfd0c1c..03fffb62 100644 --- a/src/GameCult.Caching/Contracts/cultcache-persistence-format.md +++ b/src/GameCult.Caching/Contracts/cultcache-persistence-format.md @@ -46,8 +46,11 @@ store. The store commits the batch as one durable step. Nothing in the batch is visible, to the committing flow or to observers, until the store has accepted it; a failed commit changes nothing on disk or in memory. -A batch may carry conditions: per-record `(schemaId, storedAt)` identity, or -the whole store unchanged since it was last loaded. Conditions are evaluated +A batch may carry conditions: per-record version, or the whole store unchanged +since it was last loaded. A record's version is the SHA-256 of its persisted +encoding, the array `[key, schemaId, storedAt, payload(, variant)]` a directory +page holds; it names the schema id and storedAt as well as the bytes. storedAt is +metadata and decides no condition. Conditions are evaluated under the store's exclusive lock against what is durably on disk. A failed condition is a lost race, not an error: the commit reports it and writes nothing. @@ -58,8 +61,9 @@ applies only what it staged (writes and removals) and, for a commit, its batch, and replaces the store with the result. Every other record, and every catalog entry the result references, is copied forward as stored: key, schema id, storedAt, payload and variant, and a catalog entry's exact bytes, unknown -fields included. No type interprets a copied record: a write reads other -records' headers (key, schema id, storedAt, variant slot), never their payloads. +fields included. No type interprets a copied record: a write may hash another +record's bytes to compare versions, and reads its header (key, schema id, variant +slot), but never decodes its payload. A record written over one the store holds gets a storedAt later than the one it replaces. A document changed in place and not staged is not written. @@ -72,7 +76,7 @@ the keys it depends on. **A store that holds a variant, or would after the write, is written only from what its writer last read.** A variant resolves against other records (its base chain, and the index values it shares the store with). So before writing, the -writer compares every record header in the store with the headers it last read +writer compares every record's version with the version it last read or wrote. If any record moved, appeared or disappeared, the whole write is refused with the typed write conflict, naming the keys it would have written and the keys that moved, and nothing is written. A writer that has read nothing, @@ -552,8 +556,8 @@ The v1 concurrent single-file policy is: latest snapshot and write nothing if one fails. In a store with no variant, a plain flush and an unconditional commit compare nothing: they apply their staged writes onto the current snapshot, last-writer-wins per staged key. A - write into a store that holds or would hold a variant compares every record - header with what the writer last read, and is refused if any moved. One whose + write into a store that holds or would hold a variant compares every record's + version with what the writer last read, and is refused if any moved. One whose header they cannot read is refused untouched. 5. Writers write a temp file, flush it, atomically replace the `.cc` file, and release the lock. diff --git a/src/GameCult.Caching/Contracts/cultcache-store-composition.md b/src/GameCult.Caching/Contracts/cultcache-store-composition.md index 33945f32..70a8da1d 100644 --- a/src/GameCult.Caching/Contracts/cultcache-store-composition.md +++ b/src/GameCult.Caching/Contracts/cultcache-store-composition.md @@ -222,12 +222,12 @@ against what is durably on disk at that moment: constrained; unrelated concurrent writes do not fail the commit. Passing any instance other than the one the cache holds throws. - `ExpectUnchanged()`: the store's persisted record set must equal what this - cache last loaded from it, compared as the ordered list of - `(key, schemaId, storedAt)`. Any insert, delete or replace fails it. + cache last loaded from it, compared as the ordered list of `(key, version)`. + Any insert, delete or replace fails it. -Record identity for conditions is `(schemaId, storedAt)`. It is sound because -every write to a key mints a `storedAt` strictly later than the record it -replaces; writers bump a minted timestamp by one tick when it is not later. +Record identity for conditions is the record's version, the SHA-256 of its stored +bytes (cultcache-persistence-format.md). Any write that changes a record changes +its version, whatever storedAt it carries. - A failed condition is a lost race, not an error: `Commit` returns false and nothing is written, changed in memory, or published. `TryCommit` makes one @@ -268,8 +268,7 @@ refuses to replace a file whose header it cannot read, and leaves it as it was. - A conditional commit lands the batch onto the file as it is under the lock. A store is written by one runtime at a time. Whether a Rust `fs2` lock and a C# -exclusive open of the same lock file exclude each other is not established, and -another runtime need not obey the `storedAt` rule. +exclusive open of the same lock file exclude each other is not established. ## Serialization options