Tests: non-home stores judge checks under their lock, Contended before the home writes, refusal-listed foreign version, outside store versions foreign records
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1893410fe1
commit
045db8f98a
2 changed files with 151 additions and 2 deletions
|
|
@ -5,6 +5,42 @@ using MessagePack;
|
|||
// RefKeyedDictionaryCompilesInAConsumer: this project building is the test.
|
||||
System.Console.WriteLine(new ConsumerFaction().Standing.Count);
|
||||
|
||||
// An outside store kind versions its foreign records: this project has no InternalsVisibleTo, so building and running it pins that
|
||||
// Foreign(record, catalog, version) is all a store kind outside the library needs for ExpectUnchanged to hold over them.
|
||||
using var outside = new CultCache(CultDocumentRegistry.ForTypes(new[] { typeof(ConsumerFaction) }));
|
||||
outside.AddBackingStore(new OutsideStore(), typeof(ConsumerFaction));
|
||||
var committed = outside.Commit(batch =>
|
||||
{
|
||||
batch.ExpectUnchanged();
|
||||
batch.Upsert(new ConsumerFaction { Name = "a" }, new CultRecordHandle<ConsumerFaction>(new CultRecordKey("a")));
|
||||
});
|
||||
if (!committed)
|
||||
return 1;
|
||||
return 0;
|
||||
|
||||
// Lists one record under an id no type claims, at version "v1", and commits only when the conditions hold over it.
|
||||
public sealed class OutsideStore : CacheBackingStore
|
||||
{
|
||||
private static readonly CultSchemaCatalogEntry Entry = new() { SchemaId = "outside.unclaimed", SchemaName = "outside.unclaimed" };
|
||||
|
||||
public override void PullAll()
|
||||
{
|
||||
var record = new CultPersistedRecord { Key = "w", SchemaId = Entry.SchemaId, StoredAt = "t" };
|
||||
SetForeignRecords(new[] { Foreign(record, new[] { Entry }, "v1")! });
|
||||
}
|
||||
|
||||
public override void Push(CultStoredDocument entry) { }
|
||||
|
||||
public override void Delete(CultStoredDocument entry) { }
|
||||
|
||||
public override void PushAll() { }
|
||||
|
||||
public override CultCommitOutcome CommitBatch(CultCommitRequest request, bool wait) =>
|
||||
request.ConditionsHold(new Dictionary<string, string> { ["w"] = "v1" }, Entries.Values, ForeignRecords)
|
||||
? CultCommitOutcome.Committed
|
||||
: CultCommitOutcome.Mismatch;
|
||||
}
|
||||
|
||||
[CultDocument("tests.consumer_faction", "tests.consumer_faction.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class ConsumerFaction
|
||||
|
|
|
|||
|
|
@ -1334,9 +1334,9 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That(cache.Remove(new CultRecordKey("absent")), Is.False);
|
||||
}
|
||||
|
||||
// A batch that only checks keys commits through the one writable store; every other store answers the checks from its view.
|
||||
// A batch that only checks keys commits through the one writable store; a read-only store answers the checks from its last load.
|
||||
[Test]
|
||||
public void AStoreThatIsNotTheHomeAnswersTheChecksFromItsView([Values] bool directory)
|
||||
public void AReadOnlyStoreAnswersTheChecksFromItsView([Values] bool directory)
|
||||
{
|
||||
var writable = PathOf("home.cc");
|
||||
using (var seed = Open(writable, Full, directory))
|
||||
|
|
@ -1396,5 +1396,118 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That((refusal.RecordKey, refusal.SchemaId), Is.EqualTo((W.Value, WidgetId)));
|
||||
try { cache.Dispose(); } catch (CultSchemaConflictException) { }
|
||||
}
|
||||
|
||||
// Two writable stores: home.cc holds a deck and takes Deck, other.cc holds a gizmo and takes Gizmo, and, when asked, a widget at
|
||||
// w that no registered type declares, so the cache lists it foreign at the load.
|
||||
private (CultCache Cache, string Home, string Other) TwoWritableStores(bool directory, bool otherListsWidget)
|
||||
{
|
||||
var home = PathOf("home.cc");
|
||||
using (var seed = Open(home, DeckOnly, directory))
|
||||
seed.Commit(batch => batch.Upsert(Deck, DeckOf("d"), D));
|
||||
var other = PathOf("other.cc");
|
||||
using (var seed = Open(other, CultDocumentRegistry.ForTypes(new[] { Gizmo, Widget }), directory))
|
||||
{
|
||||
seed.Commit(batch => batch.Upsert(Gizmo, New(Gizmo, ("Tag", "g")), new CultRecordKey("g")));
|
||||
if (otherListsWidget)
|
||||
seed.Commit(batch => batch.Upsert(Widget, WidgetOf("w"), W));
|
||||
}
|
||||
|
||||
var cache = new CultCache(CultDocumentRegistry.ForTypes(new[] { Deck, Gizmo }));
|
||||
cache.AddBackingStore(StoreAt(home, directory, readOnly: false), Deck);
|
||||
cache.AddBackingStore(StoreAt(other, directory, readOnly: false), Gizmo);
|
||||
return (cache, home, other);
|
||||
}
|
||||
|
||||
// Every writable store judges a batch's checks under its own lock, whichever store the batch commits through: a record another
|
||||
// writer put at a checked key in a store the batch does not write, after the load, refuses the whole commit and writes nothing.
|
||||
[Test]
|
||||
public void EveryWritableStoreRefusesACommitThatRemovesItsLateForeignRecord([Values] bool directory, [Values] bool withWrite)
|
||||
{
|
||||
var (cache, home, other) = TwoWritableStores(directory, otherListsWidget: false);
|
||||
using var _ = cache;
|
||||
using (var late = Open(other, CultDocumentRegistry.ForTypes(new[] { Gizmo, Widget }), directory))
|
||||
late.Commit(batch => batch.Upsert(Widget, WidgetOf("late"), W));
|
||||
var homeBefore = Fingerprint(home);
|
||||
var otherBefore = Fingerprint(other);
|
||||
|
||||
var refusal = Assert.Throws<CultSchemaConflictException>(() => cache.Commit(batch =>
|
||||
{
|
||||
if (withWrite)
|
||||
batch.Upsert(Deck, DeckOf("d2"), new CultRecordKey("d2"));
|
||||
batch.Remove(W);
|
||||
}))!;
|
||||
|
||||
Assert.That((refusal.RecordKey, refusal.SchemaId), Is.EqualTo((W.Value, WidgetId)));
|
||||
Assert.That(Fingerprint(home), Is.EqualTo(homeBefore));
|
||||
Assert.That(Fingerprint(other), Is.EqualTo(otherBefore));
|
||||
Assert.That(Read(home).Records.Select(record => record.Key), Does.Not.Contain("d2"));
|
||||
Assert.That(cache.BackingStores[1].ForeignRecords.Select(record => record.Key), Is.EqualTo(new[] { "w" }));
|
||||
}
|
||||
|
||||
// The non-home judgement reads the store under its lock, not its listing: a foreign record that store listed at the load,
|
||||
// since replaced by a declared one, no longer refuses the commit and is not removed.
|
||||
[Test]
|
||||
public void ANonHomeStoreJudgesACheckUnderItsLockNotFromItsLoad([Values] bool directory)
|
||||
{
|
||||
var (cache, home, other) = TwoWritableStores(directory, otherListsWidget: true);
|
||||
using var _ = cache;
|
||||
Assert.That(cache.BackingStores[1].ForeignRecords.Select(record => record.Key), Is.EqualTo(new[] { "w" }));
|
||||
using (var late = Open(other, CultDocumentRegistry.ForTypes(new[] { Gizmo, Widget }), directory))
|
||||
late.Commit(batch => batch.Upsert(Gizmo, New(Gizmo, ("Tag", "theirs")), W));
|
||||
var theirs = Stored(other, W);
|
||||
|
||||
Assert.That(cache.Commit(batch =>
|
||||
{
|
||||
batch.Upsert(Deck, DeckOf("d2"), new CultRecordKey("d2"));
|
||||
batch.Remove(W);
|
||||
}), Is.True);
|
||||
|
||||
Assert.That(Read(home).Records.Select(record => record.Key), Does.Contain("d2"));
|
||||
Assert.That(Stored(other, W), Is.EqualTo(theirs), "the declared record the checked key now holds is not removed");
|
||||
}
|
||||
|
||||
// A store that is not the batch's home and cannot take its lock makes the commit contended before the home writes.
|
||||
[Test]
|
||||
public void ANonHomeStoreHeldByAnotherWriterMakesTryCommitContended()
|
||||
{
|
||||
var (cache, home, other) = TwoWritableStores(directory: false, otherListsWidget: false);
|
||||
using var _ = cache;
|
||||
var homeBefore = Fingerprint(home);
|
||||
|
||||
using (new FileStream(other + ".lock", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
|
||||
{
|
||||
var outcome = cache.TryCommit(batch =>
|
||||
{
|
||||
batch.Upsert(Deck, DeckOf("d2"), new CultRecordKey("d2"));
|
||||
batch.Remove(W);
|
||||
});
|
||||
|
||||
Assert.That(outcome, Is.EqualTo(CultCommitOutcome.Contended));
|
||||
}
|
||||
|
||||
Assert.That(Fingerprint(home), Is.EqualTo(homeBefore));
|
||||
}
|
||||
|
||||
// A foreign record a refusal lists carries the version the refusal read, so a conditional commit over the stores that
|
||||
// list it holds when nothing has changed since.
|
||||
[Test]
|
||||
public void ExpectUnchangedHoldsAfterARefusalListedTheForeignRecord([Values] bool directory)
|
||||
{
|
||||
var path = PathOf("refusal-listed.cc");
|
||||
using (var seed = Open(path, Full, directory))
|
||||
seed.Commit(batch => batch.Upsert(Deck, DeckOf("d"), D));
|
||||
using var cache = Open(path, DeckOnly, directory);
|
||||
using (var late = Open(path, Full, directory))
|
||||
late.Commit(batch => batch.Upsert(Widget, WidgetOf("late"), W));
|
||||
Assert.That(cache.Remove(W), Is.False);
|
||||
Assert.Throws<CultSchemaConflictException>(() => cache.FlushAsync().GetAwaiter().GetResult());
|
||||
Assert.That(StoreOf(cache).ForeignRecords.Select(record => record.Key), Is.EqualTo(new[] { "w" }));
|
||||
|
||||
Assert.That(cache.Commit(batch =>
|
||||
{
|
||||
batch.ExpectUnchanged();
|
||||
batch.Upsert(Deck, DeckOf("d2"), new CultRecordKey("d2"));
|
||||
}), Is.True);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue