From ebd66e6f5b586e320e1e9bfda1a38ce48011b87d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 05:04:37 +0200 Subject: [PATCH] C# write set: a record written over another is stored later than the one it replaces ApplyWriteSet re-mints an upsert's storedAt when the durable record it replaces is not earlier, so A-B-A on the same wall clock cannot read as unchanged. Two stale whole-view comments are corrected. Co-Authored-By: Claude Sonnet 5.5 --- src/GameCult.Caching/CultCache.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs index 227710a6..4bac6b69 100644 --- a/src/GameCult.Caching/CultCache.cs +++ b/src/GameCult.Caching/CultCache.cs @@ -504,7 +504,7 @@ namespace GameCult.Caching public CultRecordKey Key { get; } // When the record was stored. With StoredSchemaId it names the record's stored bytes: a write that stores the record - // differently mints it again, including a whole-view write that re-encodes it. + // mints it a storedAt later than the one it replaces. public string StoredAt { get; internal set; } public CultDocumentDescriptor Descriptor { get; } @@ -1867,7 +1867,7 @@ namespace GameCult.Caching // durable is what the store holds on disk now; observed is what its cache last loaded or committed. // Identity is (stored schema id, storedAt), compared exactly. It names the stored bytes: every write that stores a record - // differently mints it a later storedAt, including a whole-view write that re-encodes a record it loaded. + // mints it a storedAt later than the one it replaces. public bool ConditionsHold(IReadOnlyCollection durable, IEnumerable observed) { var byKey = durable.ToDictionary(record => record.Key, StringComparer.Ordinal); @@ -3920,6 +3920,14 @@ namespace GameCult.Caching private IReadOnlyCollection ApplyWriteSet(CultPersistedStoreSnapshot? durable, IReadOnlyCollection upserts, IReadOnlyCollection removals) { RefuseWriteIntoMovedVariantStore(durable, upserts, removals); + // A record written over one the store holds is stored at a storedAt later than the one it replaces, whoever wrote that. + var replaced = (durable?.Records ?? Array.Empty()).ToDictionary(record => record.Key, record => record.StoredAt, StringComparer.Ordinal); + foreach (var staged in upserts) + { + if (replaced.TryGetValue(staged.Record.Key, out var durableAt) && !StoredLater(staged.Record.StoredAt, durableAt)) + staged.Record.StoredAt = MintStoredAt(durableAt); + } + var written = upserts.Select(staged => staged.Record.Key).ToHashSet(StringComparer.Ordinal); var removed = removals.ToHashSet(StringComparer.Ordinal); var copied = (durable?.Records ?? Array.Empty()) @@ -3955,6 +3963,11 @@ namespace GameCult.Caching return upserts; } + private static bool StoredLater(string storedAt, string than) => + DateTimeOffset.TryParse(storedAt, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var later) && + DateTimeOffset.TryParse(than, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var earlier) && + later > earlier; + private void RefuseWriteIntoMovedVariantStore(CultPersistedStoreSnapshot? durable, IReadOnlyCollection upserts, IReadOnlyCollection removals) { var records = durable?.Records ?? Array.Empty();