CultCache: a single-file version is the hash of the record's exact stored bytes, and a copied record keeps them

A record read from a store file carries the bytes it was read from (CultPersistedRecord.RawBytes, dropped when any field is
assigned) and is written as those bytes, the way a schema catalog entry already is. A version is therefore the SHA-256 of the
slice the file holds, whatever legal encoding its writer chose, not of C#'s re-encoding, and a write that copies a record leaves
its bytes and so its version alone. The contract says exactly that, so the parity cuts port it literally.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-03 09:48:24 +02:00
commit 6001ebf932
3 changed files with 35 additions and 12 deletions

View file

@ -306,6 +306,12 @@ public static class CultDocumentMessagePackSerialization
private static void WritePersistedRecord(ref MessagePackWriter writer, CultPersistedRecord record)
{
if (record.RawBytes is { } raw)
{
writer.WriteRaw(new ReadOnlySpan<byte>(raw));
return;
}
writer.WriteArrayHeader(record.Variant == null ? PersistedRecordFieldCount : VariantRecordFieldCount);
writer.Write(record.Key);
writer.Write(record.SchemaId);
@ -389,6 +395,7 @@ public static class CultDocumentMessagePackSerialization
public static CultPersistedRecord ReadPersistedRecord(ref MessagePackReader reader)
{
var start = reader.Position;
var fieldCount = reader.ReadArrayHeader();
var record = new CultPersistedRecord();
@ -429,6 +436,7 @@ public static class CultDocumentMessagePackSerialization
}
}
record.RawBytes = reader.Sequence.Slice(start, reader.Position).ToArray();
return record;
}

View file

@ -47,10 +47,14 @@ 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 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
since it was last loaded. A record's version is the SHA-256 of the exact bytes the
record is stored as: in a single file the record's own slice of the file, the array
`[key, schemaId, storedAt, payload(, variant)]` exactly as the file holds it, in
whatever legal MessagePack encoding its writer chose (never a re-encoding of the
decoded record); in a directory store the page the manifest indexes. A record being
written has the version of the bytes it is written as. It names the schema id and
storedAt as well as the payload. 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.
@ -59,9 +63,9 @@ nothing.
the store's exclusive lock the writer reads what is durably in the store now,
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 may hash another
entry the result references, is copied forward as stored: a record's exact bytes
and a catalog entry's exact bytes, unknown fields included, so a copied record
keeps its version. 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

View file

@ -134,13 +134,24 @@ namespace GameCult.Caching
public sealed class CultPersistedRecord
{
public string Key { get; set; } = string.Empty;
public string SchemaId { get; set; } = string.Empty;
public string StoredAt { get; set; } = string.Empty;
public byte[] Payload { get; set; } = Array.Empty<byte>();
private string _key = string.Empty;
private string _schemaId = string.Empty;
private string _storedAt = string.Empty;
private byte[] _payload = Array.Empty<byte>();
private CultVariantDelta? _variant;
public string Key { get => _key; set { _key = value; RawBytes = null; } }
public string SchemaId { get => _schemaId; set { _schemaId = value; RawBytes = null; } }
public string StoredAt { get => _storedAt; set { _storedAt = value; RawBytes = null; } }
public byte[] Payload { get => _payload; set { _payload = value; RawBytes = null; } }
// Non-null for a variant record (slot 4): its base and overrides. Its payload is empty.
public CultVariantDelta? Variant { get; set; }
public CultVariantDelta? Variant { get => _variant; set { _variant = value; RawBytes = null; } }
// The exact bytes this record was read from, as a store file held them. A record is written as the bytes it was read from
// while no field has been assigned since, so a copied record stays byte-identical and its version (the SHA-256 of its
// stored bytes) is the one the file's bytes name, whatever encoding their writer chose. Assigning any field drops them.
internal byte[]? RawBytes { get; set; }
}
// One record a write puts in a single-file store: encoded, with the catalog entry that publishes the id it is stored under and