Keep the StoredPublishedBy staleness guard and pin it: a record minting relabels in place is owned by a newer version that reads its current publisher whole
The probe (a throw where a loaded record is relabelled in place) tripped 32 existing tests, so the guard is reachable, not dead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
36873e696e
commit
3e283c81d4
2 changed files with 52 additions and 8 deletions
|
|
@ -524,11 +524,12 @@ namespace GameCult.Caching
|
|||
// asked.
|
||||
internal bool StoredSchemaReadWhole { get; set; }
|
||||
|
||||
// The catalog entry that publishes StoredSchemaId in the store this record was loaded from, so staging can ask WrittenOwns the
|
||||
// question the store's judge asks, over the catalog the record came with. Null for a record this cache constructed or wrote,
|
||||
// which is stored under Descriptor's own id and published by Descriptor's entry. Only a loaded record carries it, and a loaded
|
||||
// record is replaced by a new one when written, never relabelled in place, so it always names the id StoredSchemaId holds.
|
||||
internal CultSchemaCatalogEntry? StoredPublishedBy { get; set; }
|
||||
// The catalog entry that publishes StoredSchemaId in the store this record was loaded from, with the id it was published for,
|
||||
// so staging can ask WrittenOwns the question the store's judge asks, over the catalog the record came with. Null for a record
|
||||
// this cache constructed or wrote, which is stored under Descriptor's own id and published by Descriptor's entry. A write that
|
||||
// relabels a loaded record in place (minting its element ids, then a flush) moves StoredSchemaId and leaves this behind, so a
|
||||
// reader uses it only while the id still matches StoredSchemaId.
|
||||
internal (string Id, CultSchemaCatalogEntry Entry)? StoredPublishedBy { get; set; }
|
||||
|
||||
// Names the bytes this record was read from or written as: the SHA-256 of its persisted encoding, set by the store that
|
||||
// read or wrote them. A record no store has read or written has a version of its own that equals nothing else.
|
||||
|
|
@ -3986,8 +3987,8 @@ namespace GameCult.Caching
|
|||
};
|
||||
}
|
||||
|
||||
private static CultSchemaCatalogEntry? PublishedByIn(string schemaId, IReadOnlyCollection<CultSchemaCatalogEntry> catalog) =>
|
||||
CultDocumentRegistry.PublishedBy(schemaId, catalog);
|
||||
private static (string Id, CultSchemaCatalogEntry Entry)? PublishedByIn(string schemaId, IReadOnlyCollection<CultSchemaCatalogEntry> catalog) =>
|
||||
CultDocumentRegistry.PublishedBy(schemaId, catalog) is { } entry ? (schemaId, entry) : null;
|
||||
|
||||
protected void SetLastSchemaMigrationReports(IEnumerable<CultSchemaMigrationReport> reports)
|
||||
{
|
||||
|
|
@ -4035,7 +4036,10 @@ namespace GameCult.Caching
|
|||
// record's id when it was loaded (a record this cache wrote is published by its own type). The one answer for a store-backed
|
||||
// cache's staging and a store-less cache's.
|
||||
internal static bool UpsertOwned(CultStoredDocument held, CultDocumentDescriptor written) =>
|
||||
CultDocumentRegistry.WrittenOwns(written.ToCatalogEntry(), held.StoredSchemaId, new[] { held.StoredPublishedBy ?? held.Descriptor.ToCatalogEntry() });
|
||||
CultDocumentRegistry.WrittenOwns(
|
||||
written.ToCatalogEntry(),
|
||||
held.StoredSchemaId,
|
||||
new[] { held.StoredPublishedBy is { } loaded && string.Equals(loaded.Id, held.StoredSchemaId, StringComparison.Ordinal) ? loaded.Entry : held.Descriptor.ToCatalogEntry() });
|
||||
|
||||
// The refusal of a store-less cache that would retype a key.
|
||||
internal static CultSchemaConflictException Retyped(CultStoredDocument held, string key, CultDocumentDescriptor written) => new(
|
||||
|
|
|
|||
|
|
@ -274,6 +274,36 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That(DiskRecord(path, "a").SchemaId, Is.Not.EqualTo(otherRuntimeId));
|
||||
}
|
||||
|
||||
// Minting rewrites a loaded record in place under this type's id, and the cache keeps that entry. A newer version of the type,
|
||||
// registered afterwards, reads the entry the record is now published by whole, so it owns the record: staging asks the entry
|
||||
// that publishes the id the record carries now, not the one that published the id it was loaded under.
|
||||
[Test]
|
||||
public void ANewerVersionReadsWholeTheRecordMintingRelabelledInPlace()
|
||||
{
|
||||
const string otherRuntimeId = "other.runtime.id_deck";
|
||||
var path = PathOf("mint-then-newer.cc");
|
||||
WritePreIdStore(path, null, ("a", "a", 1));
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
|
||||
var entry = snapshot.SchemaCatalog.Single();
|
||||
entry.SchemaId = otherRuntimeId;
|
||||
entry.ContentHash = otherRuntimeId;
|
||||
entry.CompatibleSchemaIds = new[] { otherRuntimeId };
|
||||
snapshot.Records.Single().SchemaId = otherRuntimeId;
|
||||
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
||||
var registry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) });
|
||||
var key = new CultRecordKey("a");
|
||||
|
||||
using (var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = registry }))
|
||||
{
|
||||
Assert.That(cache.MintElementIds(), Is.EqualTo(1));
|
||||
Assert.That(cache.GetStored(key)!.StoredSchemaId, Is.Not.EqualTo(otherRuntimeId), "minting relabelled the loaded entry in place");
|
||||
registry.GetRequired(typeof(IdDeckNext));
|
||||
Assert.That(cache.Commit(batch => batch.Upsert(typeof(IdDeckNext), new IdDeckNext { Name = "newer" }, key)), Is.True);
|
||||
}
|
||||
|
||||
Assert.That(DiskRecord(path, "a").SchemaId, Is.EqualTo(registry.GetRequired(typeof(IdDeckNext)).SchemaId));
|
||||
}
|
||||
|
||||
// A pre-id record this cache reads only by dropping a persisted slot is read-only, so the rewrite that would persist its
|
||||
// minted ids is refused typed, naming it, and the store is left byte for byte.
|
||||
[Test]
|
||||
|
|
@ -1401,6 +1431,16 @@ namespace GameCult.Caching.Tests
|
|||
[Key(2)] public List<IdMove> Moves { get; set; } = new();
|
||||
}
|
||||
|
||||
// The next version of IdDeck: the same slots under a new schema version.
|
||||
[CultDocument("tests.element_id_deck", "tests.element_id_deck.v2")]
|
||||
[MessagePackObject]
|
||||
public sealed class IdDeckNext
|
||||
{
|
||||
[Key(0)] [CultName] public string Name { get; set; } = "";
|
||||
[Key(1)] public List<IdReel> Reels { get; set; } = new();
|
||||
[Key(2)] public List<IdMove> Moves { get; set; } = new();
|
||||
}
|
||||
|
||||
// The shapes before ids: same slots, no id member. Not documents, so never registered.
|
||||
[MessagePackObject]
|
||||
public sealed class OldMark
|
||||
|
|
|
|||
Loading…
Reference in a new issue