C# store: delete lay-back, WholeView, SameStoredBytes and the whole-view lay-back tests and contract text (does not build)
_unowned with its rebuild, Owns, Publisher, WholeViewWrite, WholeView, SameStoredBytes and WroteWholeView are deleted before the write-set apply that replaces them; PushAll and CommitBatchCore still call them, so this commit does not build. The contract states the write-set rule that the next commit implements. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
705ab669b5
commit
60735753dd
5 changed files with 42 additions and 358 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<string, (CultStoredDocument Loaded, CultPersistedRecord Record, CultSchemaCatalogEntry Entry, byte[]? Payload)> _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<CultSchemaCatalogEntry> 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<CultPersistedRecord>();
|
||||
public CultSchemaCatalogEntry[] Arrived = Array.Empty<CultSchemaCatalogEntry>();
|
||||
// Keys written as the file holds or held them, not as this cache holds them.
|
||||
public HashSet<string> LaidBack = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private void RefuseForeign(
|
||||
IEnumerable<string> written,
|
||||
IReadOnlyDictionary<string, (CultForeignRecord Foreign, CultPersistedRecord Record)> foreign) =>
|
||||
|
|
@ -3867,51 +3821,6 @@ namespace GameCult.Caching
|
|||
IsDirty = _staged.Count > 0;
|
||||
});
|
||||
|
||||
private WholeViewWrite WholeView(
|
||||
IEnumerable<CultStoredDocument> view,
|
||||
CultPersistedStoreSnapshot? disk,
|
||||
IReadOnlyDictionary<string, (CultForeignRecord Foreign, CultPersistedRecord Record)> 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<CultSchemaCatalogEntry>();
|
||||
var onDisk = (disk?.Records ?? Array.Empty<CultPersistedRecord>()).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<CultPersistedRecord>().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<CultSchemaCatalogEntry>()).Where(entry => !laidBackIds.Contains(entry.SchemaId)))
|
||||
.ToArray();
|
||||
return write;
|
||||
}
|
||||
|
||||
// The records the file holds that no registered type claims, by key.
|
||||
private Dictionary<string, (CultForeignRecord Foreign, CultPersistedRecord Record)> 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<CultPersistedRecord> written, HashSet<string> 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.
|
||||
|
|
|
|||
|
|
@ -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<IdDeck>(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<CultPersistedRecord>();
|
||||
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<IdDeck>(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<IdDeck>(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<IdDeck>(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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue