diff --git a/src/GameCult.Caching/Contracts/cultcache-persistence-format.md b/src/GameCult.Caching/Contracts/cultcache-persistence-format.md index d7a5bf6c..458ab42d 100644 --- a/src/GameCult.Caching/Contracts/cultcache-persistence-format.md +++ b/src/GameCult.Caching/Contracts/cultcache-persistence-format.md @@ -52,14 +52,22 @@ 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. -**Conditional commit is the only safe multi-process write.** A plain flush and -an unconditional commit are last-writer-wins and write the same bytes: a single -file is replaced by the writer's whole view, a directory manifest takes the -writer's staged keys over the current manifest. An unconditional commit also -persists writes staged earlier and leaves the store clean. Processes sharing a -store must all use conditional commit. The last writer wins only over a file it -can read: a single-file flush or commit refuses a file whose header it cannot -read (truncated, or a format it does not read) and leaves the file as it was. +**A flush, a commit and a single-file document write apply a write set.** Under +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 plain flush and an +unconditional commit write the same bytes and are last-writer-wins only per +staged key; a document changed in place and not staged is not written. An +unconditional commit also persists writes staged earlier and leaves the store +clean. Conditional commit is how a writer learns that another writer moved the +keys it depends on. A writer refuses a file whose header it cannot read +(truncated, or a format it does not read) and leaves the file as it was. C# +applies this to single-file stores, directory stores and CultMesh's single-file +document writes; Rust, TypeScript and Python rewrite the store from their own copy until their +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 @@ -142,33 +150,29 @@ names a local type only when no local type has the id, for a store written by a schema ids this one cannot know (a Rust type's schema id is its entry type). A schema renamed under a stable id therefore opens in every runtime that holds the id. -Records and catalog entries a runtime does not own survive its writes. A record a cache resolved -through the catalog's schema name, or through an entry pointing at a local id, is readable, but its -id is not one its type owns or declares compatible: a write of another record, a whole-view write -included, lays it back with its id, storedAt, bytes and catalog entry exactly as they were stored. -Only a write of that record itself stores it under the type's own id. A writer that describes -schema ids itself (Rust, whose catalog entries are derived from its entry types) lays back the entry -the file holds for an id it rewrites, and describes an id only when the file has no entry for it or -when its registered type owns the id under another name (a rename). +A write copies forward what it did not stage. A record a cache resolved through the catalog's schema +name, or through an entry pointing at a local id, is readable, but its id is not one its type owns +or declares compatible: a write of another record copies it with its id, storedAt, bytes and +catalog entry exactly as they were stored. Only a write of that record itself stores it under the +type's own id. A record that resolves to no local type is a foreign record, for example one of a type this build does not have, or one renamed without declaring its old id. A store never destroys or relabels it: the store carries it byte for byte under its own id and its own catalog entry, lists it as foreign (`CacheBackingStore.ForeignRecords` -in C#), and the cache never holds it. Writes of other records proceed and lay it back as the file holds it, a -whole-view write included. A write that would replace or remove it is refused with the typed schema conflict -naming its key and id, and nothing is written. A marked header stays marked while a store carries one, because -the writer cannot see whether it holds element ids. Declaring its id on a type claims it at the next load. +in C#), and the cache never holds it. Writes of other records proceed and copy it forward as the file holds it. A write that would replace or remove it is refused with the typed schema conflict +naming its key and id, and nothing is written. A marked header stays marked while a store carries a record the writer cannot show to hold no element id. Declaring its id on a type claims it at the next load. A reader resolves a record's schema id to the entry that owns it, that entry's own `schemaId`, and only when no entry owns the id to an entry that lists it as a compatible id (the first such entry in catalog order). A writer derives the catalog from the records it writes, and from nothing else. For each distinct -schema id a record carries it chooses exactly one entry that publishes it: the entry that owns the id -(a registered schema's descriptor, else an entry the record arrived with), else an entry that lists -it as compatible (again registered first). When a registered descriptor and an arrived entry share an -own id, the descriptor wins: a rename or a stale content hash takes the descriptor's name, hash, -members and compatible ids as they are. Entries are written as chosen, deduplicated by own id, with +schema id a record carries it chooses exactly one entry that publishes it. For an id the write stages +a record under, that is the entry the writer registered or passed, and a rename or a stale content hash +takes the descriptor's name, hash, members and compatible ids as they are. For every other id it is +the durable entry that owns the id, else one that lists it as compatible, carried as its exact bytes, +unknown trailing fields included. A registered entry never publishes an id the write stages nothing +under. Entries are written as chosen, deduplicated by own id, with no union of compatible ids and no dependence on the order records arrive in; an entry no record needs is not written. A write is refused, and the store left as it was, when two entries of one tier share an own id and disagree on the schema name, when records of different types share a schema id, when a @@ -522,8 +526,9 @@ The v1 concurrent single-file policy is: 3. Writers re-read the current snapshot after taking the lock. 4. Writers using conditional commit evaluate their conditions against the latest snapshot and write nothing if one fails. A plain flush and an - unconditional commit compare nothing and are last-writer-wins over a file - they can read; one whose header they cannot read is refused untouched. + unconditional commit compare nothing: they apply their staged writes onto the + current snapshot, last-writer-wins per staged key; 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 5b0cb5a0..3b543f5c 100644 --- a/src/GameCult.Caching/Contracts/cultcache-store-composition.md +++ b/src/GameCult.Caching/Contracts/cultcache-store-composition.md @@ -248,19 +248,16 @@ replaces; writers bump a minted timestamp by one tick when it is not later. match its content address, at most five times; a pull that has not settled by then throws. -**A plain flush and an unconditional commit are last-writer-wins: per file for -single-file stores, per key for directory stores.** Both run -under the same lock, so two writers never interleave bytes, but they compare -nothing. Only a conditional commit (`Expect` or `ExpectUnchanged`) protects -against another writer; processes sharing a store must all use it. A writer -wins only over a file it can read: a single-file store refuses to replace a file -whose header it cannot read, and leaves it as it was. +**A plain flush and an unconditional commit are last-writer-wins per staged +key, in both store kinds.** Both run under the same lock, so two writers never +interleave bytes, but they compare nothing. Only a conditional commit +(`Expect` or `ExpectUnchanged`) tells a writer another writer moved the keys it +depends on. A writer wins only over a file it can read: a single-file store +refuses to replace a file whose header it cannot read, and leaves it as it was. -- A single-file store writes this cache's whole view of the file: a record - another writer added since this cache last pulled is gone. -- A directory store writes this cache's staged keys onto the current manifest: - another writer's unrelated keys survive, a key both wrote holds the last - write. +- A single-file store and a directory store apply this cache's staged writes and + removals onto what the store holds now: another writer's unrelated records + survive as stored, a key both wrote holds the last write. - An unconditional commit writes exactly what a flush of the same staged state plus the batch would write. It also persists any single writes staged earlier in that store, and leaves the store clean. diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs index 97d938fe..499ee0e0 100644 --- a/src/GameCult.Caching/CultCache.cs +++ b/src/GameCult.Caching/CultCache.cs @@ -3701,37 +3701,7 @@ namespace GameCult.Caching Entries[stored.Key.Value] = stored; SetLastSchemaMigrationReports(reports); SetForeignRecords(foreign); - - _unowned.Clear(); - foreach (var record in snapshot.Records) - { - if (!Entries.TryGetValue(record.Key, out var held) || Owns(held.Descriptor, record.SchemaId)) - continue; - // An entry under the type's own id that lists the record's id says the record is this schema's: it is re-encoded. - var publisher = Publisher(record.SchemaId, snapshot.SchemaCatalog); - if (!Owns(held.Descriptor, publisher.SchemaId)) - _unowned[record.Key] = (held, record, publisher, held.Variant == null ? SerializePayload(held.Document) : null); - } } - - // The records this store loaded under an id their type neither owns nor lists as compatible (another runtime's id, or an - // earlier declaration of the schema, resolved through the catalog's name or its pointer to a local id), each with the - // catalog entry that published it, keyed while the store holds the entry it loaded. A whole-view write lays such a record - // back exactly as it was stored, id and catalog entry included; only a write of the record itself stores it under its - // type's own id. Payload is what a plain document serialized to when it was loaded, so a change made to the document in - // place is told from a record nobody wrote (a variant persists its delta, never its resolved document). - private readonly Dictionary _unowned = - new(StringComparer.Ordinal); - - private static bool Owns(CultDocumentDescriptor descriptor, string schemaId) => - string.Equals(descriptor.SchemaId, schemaId, StringComparison.Ordinal) || - descriptor.CompatibleSchemaIds.Contains(schemaId, StringComparer.Ordinal); - - // The entry that publishes a schema id: the one that owns it, else the first that lists it. - private static CultSchemaCatalogEntry Publisher(string schemaId, IReadOnlyCollection catalog) => - catalog.FirstOrDefault(entry => string.Equals(entry.SchemaId, schemaId, StringComparison.Ordinal)) - ?? catalog.First(entry => entry.CompatibleSchemaIds.Contains(schemaId, StringComparer.Ordinal)); - public override void Push(CultStoredDocument entry) { ThrowIfReadOnly(); @@ -3839,22 +3809,6 @@ namespace GameCult.Caching return CultCommitOutcome.Committed; } - // A record's (schema id, storedAt) names its stored bytes, and a commit condition trusts that name. So a whole-view write - // that stores a record differently is a new store of it and mints it a later storedAt: under another id than it was loaded - // under (a type re-encoding a record it read under an older id it declares), or with other - // bytes than the file holds at that storedAt. A record written exactly as it is stored keeps its storedAt. - // Records and catalog entries this runtime does not own survive the write: a record loaded under an id its type does not own - // and that nothing has written since is laid back as it was loaded, with the catalog entry that published it, and every - // foreign record the file holds now is laid back as the file holds it, over this cache's clean copy of it; a write this cache staged over one - // was refused before this. Arrived is the catalog the write publishes those records by. - private sealed class WholeViewWrite - { - public CultPersistedRecord[] Records = Array.Empty(); - public CultSchemaCatalogEntry[] Arrived = Array.Empty(); - // Keys written as the file holds or held them, not as this cache holds them. - public HashSet LaidBack = new(StringComparer.Ordinal); - } - private void RefuseForeign( IEnumerable written, IReadOnlyDictionary foreign) => @@ -3867,51 +3821,6 @@ namespace GameCult.Caching IsDirty = _staged.Count > 0; }); - private WholeViewWrite WholeView( - IEnumerable view, - CultPersistedStoreSnapshot? disk, - IReadOnlyDictionary foreign) - { - // A record the file holds as foreign is the file's: it is carried, and this cache's clean copy of it is not written. - var entries = view.Where(entry => !foreign.ContainsKey(entry.Key.Value)).ToArray(); - - var write = new WholeViewWrite(); - write.LaidBack.UnionWith(foreign.Keys); - var laidBackEntries = new List(); - var onDisk = (disk?.Records ?? Array.Empty()).ToDictionary(record => record.Key, StringComparer.Ordinal); - write.Records = entries.Select(entry => - { - if (_unowned.TryGetValue(entry.Key.Value, out var loaded) && ReferenceEquals(loaded.Loaded, entry) && - (loaded.Payload == null || SerializePayload(entry.Document).AsSpan().SequenceEqual(loaded.Payload))) - { - // Laid back only while the file still holds what was loaded: after that the file's record is its owner's newer - // write, which stays as it is, and a record the file no longer holds is not written back. - write.LaidBack.Add(entry.Key.Value); - if (!onDisk.TryGetValue(entry.Key.Value, out var current)) - return null; - if (current.SchemaId != loaded.Record.SchemaId || current.StoredAt != loaded.Record.StoredAt) - return current; - laidBackEntries.Add(loaded.Entry); - return loaded.Record; - } - - var record = ToPersistedRecord(entry, SerializePayload); - var unchanged = onDisk.TryGetValue(record.Key, out var stored) && stored.StoredAt == record.StoredAt - ? SameStoredBytes(stored, record) - : entry.StoredSchemaId == record.SchemaId; - if (!unchanged) - record.StoredAt = CultCache.MintStoredAt(record.StoredAt); - return record; - }).OfType().Concat(foreign.Values.Select(carried => carried.Record)).ToArray(); - - // An entry that published a laid-back record is written as it was loaded, whatever the file's entry for that id says now. - var laidBackIds = laidBackEntries.Select(entry => entry.SchemaId).ToHashSet(StringComparer.Ordinal); - write.Arrived = laidBackEntries.Distinct() - .Concat((disk?.SchemaCatalog ?? Array.Empty()).Where(entry => !laidBackIds.Contains(entry.SchemaId))) - .ToArray(); - return write; - } - // The records the file holds that no registered type claims, by key. private Dictionary ForeignOnDisk(CultPersistedStoreSnapshot? disk) { @@ -3925,36 +3834,6 @@ namespace GameCult.Caching return foreign; } - private static bool SameStoredBytes(CultPersistedRecord stored, CultPersistedRecord written) => - stored.SchemaId == written.SchemaId && - stored.Payload.AsSpan().SequenceEqual(written.Payload) && - (stored.Variant == null - ? written.Variant == null - : written.Variant != null && - stored.Variant.BaseKey == written.Variant.BaseKey && - stored.Variant.Overrides.Count == written.Variant.Overrides.Count && - stored.Variant.Overrides.Zip(written.Variant.Overrides, (a, b) => - a.Op == b.Op && a.Id == b.Id && a.Value.AsSpan().SequenceEqual(b.Value) && - a.Path.Select(step => (step.Slot, step.ElementId)).SequenceEqual(b.Path.Select(step => (step.Slot, step.ElementId)))) - .All(same => same)); - - // The file now holds every record written, under the id and at the storedAt the write gave it: the store's entries and the - // cache's say so. A laid-back or carried record is on disk as the file had it, so the cache learns nothing new about it: in - // particular the ids it minted at load are still in memory only. - private void WroteWholeView(IReadOnlyCollection written, HashSet laidBack) - { - var asHeld = written.Where(record => !laidBack.Contains(record.Key)).ToArray(); - foreach (var record in asHeld) - { - if (!Entries.TryGetValue(record.Key, out var entry)) - continue; - entry.StoredSchemaId = record.SchemaId; - entry.StoredAt = record.StoredAt; - } - - Cache?.WroteWholeView(asHeld); - } - // Only a variant makes a merge more than the committer's own judgement: a variant it lands, one another writer wrote // (read from the record's header, never its payload), or one this cache holds. A plain-only store never decodes // another writer's records, so a schema this cache does not register cannot fail a commit that involves no variant. diff --git a/tests/GameCult.Caching.Tests/ElementIdTests.cs b/tests/GameCult.Caching.Tests/ElementIdTests.cs index 470ee4fc..e36ca015 100644 --- a/tests/GameCult.Caching.Tests/ElementIdTests.cs +++ b/tests/GameCult.Caching.Tests/ElementIdTests.cs @@ -212,26 +212,6 @@ namespace GameCult.Caching.Tests Assert.That(reader.MintElementIds(), Is.EqualTo(0)); } - // A record from before ids, under another runtime's id for the same schema, is laid back by a whole-view write exactly as it - // was stored. The ids it minted at load are still in memory only, so MintElementIds still rewrites it, under the type's own id. - [Test] - public void AWholeViewWriteLeavesTheLoadMintedIdsOfARecordItLaysBackInMemoryOnly() - { - var path = PathOf("unowned-pre-id.cc"); - WritePreIdStore(path, null, ("old", "o", 1)); - var ownId = DiskRecord(path, "old").SchemaId; - RelabelAsOtherRuntime(path); - var stored = DiskRecord(path, "old"); - - using var cache = Open(path); - cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other"))); - - var laidBack = DiskRecord(path, "old"); - Assert.That((laidBack.SchemaId, laidBack.StoredAt, laidBack.Payload), Is.EqualTo((stored.SchemaId, stored.StoredAt, stored.Payload))); - Assert.That(cache.MintElementIds(), Is.EqualTo(1), "its ids are still in memory only"); - Assert.That(DiskRecord(path, "old").SchemaId, Is.EqualTo(ownId), "a write of the record stores it under the type's own id"); - } - // The store's one record, stored as another runtime stores that schema: under an id the type does not own, published by a // catalog entry of its own. private static void RelabelAsOtherRuntime(string path) @@ -252,147 +232,6 @@ namespace GameCult.Caching.Tests return path; } - // The record's owner wrote it after this cache loaded it: the file holds the owner's newer record, and this cache's write of - // another record leaves it as it is, storedAt included (a record's storedAt never moves backwards). - [Test] - public void AWholeViewWriteDoesNotRevertARecordItsOwnerWroteAfterTheLoad([Values] bool flush) - { - var path = UnownedStore("owner-wrote.cc"); - using var cache = Open(path); - var loadedAt = DiskRecord(path, "old").StoredAt; - using (var owner = Open(path)) - owner.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("owner"), new CultRecordKey("old"))); - var owned = DiskRecord(path, "old"); - - if (flush) - { - cache.UpsertAsync(typeof(IdDeck), Deck("other"), new CultRecordKey("other")).GetAwaiter().GetResult(); - cache.FlushAsync().GetAwaiter().GetResult(); - } - else - cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other"))); - - var after = DiskRecord(path, "old"); - Assert.That((after.SchemaId, after.StoredAt, after.Payload), Is.EqualTo((owned.SchemaId, owned.StoredAt, owned.Payload))); - Assert.That(string.CompareOrdinal(after.StoredAt, loadedAt), Is.GreaterThan(0)); - Assert.That(MessagePackSerializer.Deserialize(after.Payload).Name, Is.EqualTo("owner")); - } - - // The owner removed it: this cache's stale copy is not written back. - [Test] - public void AWholeViewWriteDoesNotWriteBackARecordItsOwnerRemovedAfterTheLoad() - { - var path = UnownedStore("owner-removed.cc"); - using var cache = Open(path); - var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)); - snapshot.Records = Array.Empty(); - File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot)); - - cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other"))); - - Assert.That(CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).Records.Select(record => record.Key), Is.EqualTo(new[] { "other" })); - } - - // The file's record counts as moved when either half of its name (schema id, storedAt) is no longer the loaded one. - [TestCase(true, false)] - [TestCase(false, true)] - public void AWholeViewWriteCarriesTheFilesRecordWhenOnlyItsStoredAtOrOnlyItsIdMoved(bool newerStoredAt, bool ownersId) - { - var path = UnownedStore("moved.cc"); - using var cache = Open(path); - var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)); - var moved = snapshot.Records.Single(); - if (newerStoredAt) - moved.StoredAt = DateTimeOffset.UtcNow.AddMinutes(5).ToString("O"); - if (ownersId) - { - var owner = Registry.GetRequired(typeof(IdDeck)); - moved.SchemaId = owner.SchemaId; - snapshot.SchemaCatalog = snapshot.SchemaCatalog.Append(owner.ToCatalogEntry()).ToArray(); - } - File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot)); - - cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other"))); - - var after = DiskRecord(path, "old"); - Assert.That((after.SchemaId, after.StoredAt), Is.EqualTo((moved.SchemaId, moved.StoredAt))); - } - - // A record that carried no ids when it was laid back does not mark the store, though the ids it minted at load are in memory. - [Test] - public void ALaidBackRecordWithoutIdsDoesNotMarkTheStore() - { - var path = UnownedStore("laid-back-v1.cc"); - using var cache = Open(path); - - cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("empty", reels: 0), new CultRecordKey("empty"))); - - Assert.That(DiskRecord(path, "old").SchemaId, Is.EqualTo("other.runtime.deck"), "laid back"); - Assert.That(CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).FormatVersion, Is.EqualTo(CultPersistedStoreSnapshot.FormatV1)); - } - - // A change made to a laid-back document in place is a write of the record, as it is for a record the type owns: it is stored - // under the type's own id, not dropped. - [Test] - public void AnInPlaceChangeToALaidBackDocumentIsWritten() - { - var path = UnownedStore("in-place.cc"); - using var cache = Open(path); - cache.Get(new CultRecordKey("old"))!.Name = "changed"; - - cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other"))); - - var written = DiskRecord(path, "old"); - Assert.That(written.SchemaId, Is.Not.EqualTo("other.runtime.deck")); - Assert.That(MessagePackSerializer.Deserialize(written.Payload).Name, Is.EqualTo("changed")); - } - - // The catalog entry that published a laid-back record is written as it was loaded, whatever the file's entry for the id says now. - [Test] - public void ALaidBackRecordIsPublishedByTheCatalogEntryItWasLoadedWith() - { - var path = UnownedStore("laid-back-entry.cc"); - using var cache = Open(path); - var loadedHash = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).SchemaCatalog.Single().ContentHash; - var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)); - snapshot.SchemaCatalog.Single().ContentHash = "changed-by-another-writer"; - File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot)); - - cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other"))); - - var published = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).SchemaCatalog - .Where(entry => entry.SchemaId == "other.runtime.deck").ToArray(); - Assert.That(published, Has.Length.EqualTo(1)); - Assert.That(published[0].ContentHash, Is.EqualTo(loadedHash)); - } - - // A whole-view write that persists the ids a record minted at load stores other bytes under the same id: a new store of - // the record, so it takes a later storedAt, and the cache that wrote it holds that storedAt. A record it stores exactly as - // the file holds it keeps its own. - [Test] - public void AWholeViewWriteThatPersistsLoadMintedIdsGivesTheRecordALaterStoredAt() - { - var path = PathOf("restamp.cc"); - WritePreIdStore(path, "kept", ("old", "o", 1)); - var old = DiskRecord(path, "old"); - var kept = DiskRecord(path, "kept"); - - using var cache = Open(path); - var current = cache.Get(new CultRecordKey("old"))!; - cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other"))); - - var written = DiskRecord(path, "old"); - Assert.That(written.SchemaId, Is.EqualTo(old.SchemaId), "the same id"); - Assert.That(written.Payload, Is.Not.EqualTo(old.Payload), "other bytes: the ids are on disk now"); - Assert.That(string.CompareOrdinal(written.StoredAt, old.StoredAt), Is.GreaterThan(0), "so a later storedAt"); - Assert.That(DiskRecord(path, "kept").StoredAt, Is.EqualTo(kept.StoredAt), "a record stored unchanged keeps its storedAt"); - Assert.That(cache.TryCommit(batch => - { - batch.Expect(new CultRecordKey("old"), current); - batch.Upsert(typeof(IdDeck), Deck("after"), new CultRecordKey("after")); - }), Is.EqualTo(CultCommitOutcome.Committed), "the cache holds the storedAt it wrote"); - } - [Test] public void TheByteCostOfAnIdIsMeasured() { diff --git a/tests/GameCult.Caching.Tests/ForeignRecordTests.cs b/tests/GameCult.Caching.Tests/ForeignRecordTests.cs index c25fd53e..796fa0e0 100644 --- a/tests/GameCult.Caching.Tests/ForeignRecordTests.cs +++ b/tests/GameCult.Caching.Tests/ForeignRecordTests.cs @@ -603,23 +603,6 @@ namespace GameCult.Caching.Tests return path; } - // The record and the entry that described it when it loaded travel together: another writer re-describing the id since - // does not change what a whole view lays back. - [Test] - public void AWholeViewLaysBackTheCatalogEntryItLoadedWhateverTheFileSaysNow([Values(Write.Flush, Write.Commit)] Write write) - { - var path = OtherRuntimeStore("redescribed.cc"); - var loaded = CatalogEntryBytes(path, OtherRuntimeId); - using var cache = Open(path, DeckOnly, directory: false); - var snapshot = Read(path); - snapshot.SchemaCatalog.Single(entry => entry.SchemaId == OtherRuntimeId).SchemaVersion = "tests.foreign_deck.redescribed"; - File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot)); - - Land(cache, write, E, DeckOf("e")); - - Assert.That(CatalogEntryBytes(path, OtherRuntimeId), Is.EqualTo(loaded)); - } - // A store whose file is gone holds nothing, foreign records included. [Test] public void AStoreWhoseFileIsGoneListsNoForeignRecords() @@ -651,25 +634,6 @@ namespace GameCult.Caching.Tests SchemaCatalog = Read(path).SchemaCatalog.Where(entry => entry.SchemaId == schemaId).ToArray() }); - // Records and catalog entries a runtime does not own survive its writes: a write of another record lays the record back with - // its id, storedAt, bytes and catalog entry as they were stored. - [Test] - public void AWriteOfAnotherRecordLaysBackARecordUnderAnIdThisBuildDoesNotOwn([Values] Write write) - { - var path = OtherRuntimeStore("other-runtime.cc"); - var before = Stored(path, D); - var entry = CatalogEntryBytes(path, OtherRuntimeId); - - using (var cache = Open(path, DeckOnly, directory: false)) - { - Assert.That(EmittedDocumentTypes.Read(cache.Get(D)!, "Name"), Is.EqualTo("d"), "this build reads it"); - Land(cache, write, E, DeckOf("e")); - } - - Assert.That(Stored(path, D), Is.EqualTo(before)); - Assert.That(CatalogEntryBytes(path, OtherRuntimeId), Is.EqualTo(entry)); - } - // A write of the record itself stores what the cache holds, under the type's own id. [Test] public void AWriteOfTheRecordItselfStoresItUnderTheTypesOwnId([Values(Write.Flush, Write.Commit)] Write write)