Tests: version identity (rewrite at the same storedAt, pull arrival, ExpectUnchanged, staleness check, own-write version, in-memory, SHA-256 definition)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b195525d64
commit
f2b5b83345
3 changed files with 200 additions and 0 deletions
|
|
@ -258,6 +258,24 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That(reads, Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
// With no backing store a record's version is minted with it: an Expect on the held record commits, and once the record is
|
||||
// replaced after it was observed (here by the same document again) the Expect no longer holds.
|
||||
[Test]
|
||||
public void InMemoryExpectHoldsUntilTheRecordIsReplaced()
|
||||
{
|
||||
using var cache = new CultCache(CultDocumentRegistry.ForTypes(new[] { typeof(Counter) }));
|
||||
cache.Commit(batch => batch.Upsert(new Counter { Name = "counter", Value = 1 }, new CultRecordHandle<Counter>(Key)));
|
||||
var first = cache.Get<Counter>(Key)!;
|
||||
|
||||
Assert.That(cache.Commit(batch => batch.Expect(Key, first)), Is.True);
|
||||
|
||||
Assert.That(cache.Commit(batch =>
|
||||
{
|
||||
batch.Expect(Key, first);
|
||||
cache.UpsertAsync(first, new CultRecordHandle<Counter>(Key)).GetAwaiter().GetResult();
|
||||
}), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StoredAtIsStrictlyIncreasingPerKey()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -621,6 +621,48 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That(Header(path), Is.EqualTo(header));
|
||||
}
|
||||
|
||||
// A record's version is the SHA-256 of the bytes it is stored as: in a single file the record's own slice of the file, read
|
||||
// here with the MessagePack reader and not the serializer; in a directory the hash the manifest indexes its page under.
|
||||
[Test]
|
||||
public void AVersionIsTheSha256OfTheRecordsStoredBytes([Values] bool directory)
|
||||
{
|
||||
var source = File.ReadAllBytes(Path.Combine(VectorRoot(), "..", "v3-base.msgpack"));
|
||||
var slices = new Dictionary<string, string>();
|
||||
var reader = new MessagePackReader(source);
|
||||
reader.ReadArrayHeader();
|
||||
reader.Skip();
|
||||
reader.Skip();
|
||||
var count = reader.ReadArrayHeader();
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(source);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var start = (int)reader.Consumed;
|
||||
reader.Skip();
|
||||
slices[snapshot.Records[i].Key] = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(source.AsSpan(start, (int)reader.Consumed - start))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
var path = Path.Combine(_directory, "version.cc");
|
||||
File.WriteAllBytes(path, source);
|
||||
if (directory)
|
||||
{
|
||||
// The same records, written as a directory store: its versions are the manifest's page hashes.
|
||||
File.Delete(path);
|
||||
using (var seed = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry, UseDirectoryStore = true }))
|
||||
seed.Commit(batch =>
|
||||
{
|
||||
foreach (var key in slices.Keys)
|
||||
batch.Upsert(typeof(VectorItem), new VectorItem { Name = key }, new CultRecordKey(key));
|
||||
});
|
||||
slices = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).Records
|
||||
.ToDictionary(record => record.Key, record => Convert.ToHexString(record.Payload).ToLowerInvariant());
|
||||
}
|
||||
|
||||
using var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry, UseDirectoryStore = directory });
|
||||
|
||||
Assert.That(cache.AllStoredDocuments.ToDictionary(stored => stored.Key.Value, stored => stored.StoredVersion), Is.EqualTo(slices));
|
||||
Assert.That(slices, Is.Not.Empty);
|
||||
}
|
||||
|
||||
// Every runtime's tests walk the manifest, so a vector without a row would go untested.
|
||||
[Test]
|
||||
public void EveryVectorInTheFolderHasAManifestRow()
|
||||
|
|
|
|||
|
|
@ -508,6 +508,146 @@ namespace GameCult.Caching.Tests
|
|||
Is.False, "B's copy of k is no longer what the store holds");
|
||||
}
|
||||
|
||||
// Rule 2: a record's version is the SHA-256 of its stored bytes. Another writer rewrites the record to other bytes yet leaves
|
||||
// its storedAt as it was (a peer whose clock equals, or a tool): the bytes are all that tells the two records apart.
|
||||
private void RewriteAtItsOwnStoredAt(string path, bool directory, string key, string note)
|
||||
{
|
||||
var storedAt = StoredAtOf(path, key);
|
||||
using (var writer = Open(path, directory))
|
||||
writer.Commit(batch => batch.Upsert(typeof(WsItem), new WsItem { Name = "name-" + key, Note = note }, new CultRecordKey(key)));
|
||||
var snapshot = Read(path);
|
||||
var record = snapshot.Records.Single(candidate => candidate.Key == key);
|
||||
if (!directory)
|
||||
{
|
||||
record.StoredAt = storedAt;
|
||||
Write(path, snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
var pages = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(path);
|
||||
var page = CultDocumentMessagePackSerialization.DeserializePersistedRecord(
|
||||
File.ReadAllBytes(Path.Combine(pages, Convert.ToHexString(record.Payload).ToLowerInvariant() + ".msgpack")));
|
||||
page.StoredAt = storedAt;
|
||||
var bytes = CultDocumentMessagePackSerialization.SerializePersistedRecord(page);
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
|
||||
File.WriteAllBytes(Path.Combine(pages, Convert.ToHexString(hash).ToLowerInvariant() + ".msgpack"), bytes);
|
||||
record.StoredAt = storedAt;
|
||||
record.Payload = hash;
|
||||
Write(path, snapshot);
|
||||
}
|
||||
|
||||
private static string NoteOnDisk(string path, bool directory)
|
||||
{
|
||||
using var fresh = Open(path, directory);
|
||||
return fresh.Get<WsItem>(K)!.Note;
|
||||
}
|
||||
|
||||
private static readonly CultRecordKey K = new("k");
|
||||
|
||||
// A reads k; another writer rewrites it at its own storedAt; A's Expect on what it read fails and the disk keeps the rewrite.
|
||||
[Test]
|
||||
public void AStaleExpectFailsWhenAnotherWriterRewroteTheRecordAtItsOwnStoredAt([Values] bool directory)
|
||||
{
|
||||
var path = PathOf("t1.cc");
|
||||
Seed(path, directory, "k");
|
||||
using var a = Open(path, directory);
|
||||
var observed = a.Get<WsItem>(K)!;
|
||||
RewriteAtItsOwnStoredAt(path, directory, "k", "by-c");
|
||||
|
||||
var committed = a.Commit(batch =>
|
||||
{
|
||||
batch.Expect(K, observed);
|
||||
batch.Upsert(typeof(WsItem), new WsItem { Name = "name-k", Note = "stale" }, K);
|
||||
});
|
||||
|
||||
Assert.That(committed, Is.False);
|
||||
Assert.That(NoteOnDisk(path, directory), Is.EqualTo("by-c"));
|
||||
}
|
||||
|
||||
// A pull serves the rewritten record although nothing about it but its bytes changed.
|
||||
[Test]
|
||||
public void APullServesARecordRewrittenAtItsOwnStoredAt([Values] bool directory)
|
||||
{
|
||||
var path = PathOf("t2.cc");
|
||||
Seed(path, directory, "k");
|
||||
using var a = Open(path, directory);
|
||||
RewriteAtItsOwnStoredAt(path, directory, "k", "by-c");
|
||||
|
||||
a.PullAllBackingStoresAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(a.Get<WsItem>(K)!.Note, Is.EqualTo("by-c"));
|
||||
}
|
||||
|
||||
// ExpectUnchanged compares every record's version: a rewrite at the same storedAt fails it for an unrelated key.
|
||||
[Test]
|
||||
public void ExpectUnchangedFailsWhenARecordWasRewrittenAtItsOwnStoredAt([Values] bool directory)
|
||||
{
|
||||
var path = PathOf("t3.cc");
|
||||
Seed(path, directory, "k");
|
||||
using var a = Open(path, directory);
|
||||
RewriteAtItsOwnStoredAt(path, directory, "k", "by-c");
|
||||
|
||||
var committed = a.Commit(batch =>
|
||||
{
|
||||
batch.ExpectUnchanged();
|
||||
batch.Upsert(typeof(WsItem), new WsItem { Name = "name-x" }, X);
|
||||
});
|
||||
|
||||
Assert.That(committed, Is.False);
|
||||
Assert.That(Keys(path), Is.EqualTo(new[] { "k" }));
|
||||
}
|
||||
|
||||
// The staleness check of a store holding a variant compares versions: a rewrite at the record's own storedAt refuses the
|
||||
// write, names the record, leaves the file as it was, and the cache then holds the rewrite so a retry lands.
|
||||
[Test]
|
||||
public void AWriteIntoAVariantStoreIsRefusedWhenARecordWasRewrittenAtItsOwnStoredAt()
|
||||
{
|
||||
var path = PathOf("t4.cc");
|
||||
Seed(path, false, "a");
|
||||
using var a = Open(path, false);
|
||||
AddVariant(a, "v", A);
|
||||
using (var peer = Open(path, false))
|
||||
Assert.That(peer.Get<WsItem>(A), Is.Not.Null);
|
||||
RewriteAtItsOwnStoredAt(path, false, "a", "by-c");
|
||||
var before = File.ReadAllBytes(path);
|
||||
var y = new CultRecordKey("y");
|
||||
|
||||
var conflict = Assert.Throws<CultWriteConflictException>(() =>
|
||||
Land(a, true, typeof(WsItem), new WsItem { Name = "name-y" }, y))!;
|
||||
|
||||
Assert.That(conflict.ChangedKeys, Is.EqualTo(new[] { "a" }));
|
||||
Assert.That(File.ReadAllBytes(path), Is.EqualTo(before));
|
||||
Assert.That(a.Get<WsItem>(A)!.Note, Is.EqualTo("by-c"), "the refusal reloaded the record by version");
|
||||
Land(a, true, typeof(WsItem), new WsItem { Name = "name-y" }, y);
|
||||
Assert.That(Keys(path), Is.EqualTo(new[] { "a", "v", "y" }));
|
||||
}
|
||||
|
||||
// A write teaches the writer's cache the version of the bytes it wrote, by flush or by commit, in either store kind: the
|
||||
// writer's own Expect holds without a pull, and fails once anyone else writes the record.
|
||||
[Test]
|
||||
public void AnOwnWriteTeachesTheCacheItsVersion([Values] bool directory, [Values] bool commit)
|
||||
{
|
||||
var path = PathOf("t5.cc");
|
||||
Seed(path, directory, "a");
|
||||
using var a = Open(path, directory);
|
||||
Land(a, commit, typeof(WsItem), new WsItem { Name = "name-k", Note = "by-a" }, K);
|
||||
|
||||
Assert.That(a.Commit(batch =>
|
||||
{
|
||||
batch.Expect(K, a.Get<WsItem>(K)!);
|
||||
batch.Upsert(typeof(WsItem), new WsItem { Name = "name-k", Note = "again" }, K);
|
||||
}), Is.True, "A's own condition holds without a pull");
|
||||
|
||||
var seen = a.Get<WsItem>(K)!;
|
||||
using (var b = Open(path, directory))
|
||||
b.Commit(batch => batch.Upsert(typeof(WsItem), new WsItem { Name = "name-k", Note = "by-b" }, K));
|
||||
Assert.That(a.Commit(batch =>
|
||||
{
|
||||
batch.Expect(K, seen);
|
||||
batch.Upsert(typeof(WsItem), new WsItem { Name = "name-k", Note = "stale" }, K);
|
||||
}), Is.False);
|
||||
}
|
||||
|
||||
// A commit that removes a key this cache staged removes it everywhere: the staged write does not survive the batch.
|
||||
[Test]
|
||||
public void ACommitThatRemovesAStagedKeyRemovesIt()
|
||||
|
|
|
|||
Loading…
Reference in a new issue