Caching: an empty direct flush still applies as before; only a write that checked keys and has nothing else to write writes no file. Tests: the delisting writer declares the widget; the checked-key flush also writes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-12 05:06:19 +02:00
commit cec09806b0
2 changed files with 7 additions and 3 deletions

View file

@ -4164,7 +4164,7 @@ namespace GameCult.Caching
// Flush, commit and Mesh's write all end here, with the durable store they read under the lock they hold. First, a key the write
// names or checks whose durable record is under an id the writer does not declare refuses the write (RefuseUndeclaredOnDisk); a
// write that then has nothing to write (it only checked keys) writes no file. Then a store that holds
// write that then has nothing to write but keys it checked writes no file. Then a store that holds
// a variant, or would after this write, is written only while its record versions are what this store last read: a variant
// resolves against other records, and a write decodes none it did not stage, so it cannot judge another writer's. Versions
// only: a write hashes other records' bytes, never decodes them. Otherwise the whole write is refused and what it refused is
@ -4175,7 +4175,7 @@ namespace GameCult.Caching
Durable? durable, IReadOnlyCollection<CultStagedRecord> upserts, IReadOnlyCollection<string> removals, IEnumerable<string> checks)
{
RefuseUndeclaredOnDisk(durable, upserts, removals, checks);
if (upserts.Count == 0 && removals.Count == 0)
if (upserts.Count == 0 && removals.Count == 0 && (checks.Any() || StagedChecks.Count > 0))
return Array.Empty<(CultStagedRecord, string)>();
RefuseWriteIntoMovedVariantStore(durable, upserts, removals);
var durableRecords = durable?.Snapshot.Records ?? Array.Empty<CultPersistedRecord>();

View file

@ -1125,6 +1125,7 @@ namespace GameCult.Caching.Tests
using (var seed = Open(path, Full, directory))
seed.Commit(batch => batch.Upsert(Deck, DeckOf("d"), D));
using var a = Open(path, DeckOnly, directory);
a.UpsertAsync(Deck, DeckOf("e"), E).GetAwaiter().GetResult();
Assert.That(a.Remove(W), Is.False);
using (var b = Open(path, Full, directory))
b.Commit(batch => batch.Upsert(Deck, DeckOf("theirs"), W));
@ -1132,6 +1133,7 @@ namespace GameCult.Caching.Tests
Assert.DoesNotThrow(() => a.FlushAsync().GetAwaiter().GetResult());
Assert.That(Read(path).Records.Select(record => record.Key), Is.EquivalentTo(new[] { "d", "e", "w" }), "the staged write landed beside the checked key");
Assert.That(Stored(path, W), Is.EqualTo(theirs), "the record at the checked key is still the other writer's bytes");
using (var c = Open(path, Full, directory))
c.Commit(batch => batch.Upsert(Widget, WidgetOf("now a widget"), W));
@ -1173,7 +1175,9 @@ namespace GameCult.Caching.Tests
var path = Seeded("delist.cc", directory);
using var a = Open(path, DeckOnly, directory);
Assert.That(StoreOf(a).ForeignRecords.Select(record => record.Key), Is.EqualTo(new[] { "w" }));
Move(path, W, "moved", directory);
// The writer declares the widget's id, so it may replace the widget with a deck record under an id a reads but does not declare.
using (var other = Open(path, CultDocumentRegistry.ForTypes(new[] { MovingDeck, Widget }), directory))
Assert.That(other.Commit(batch => batch.Upsert(MovingDeck, New(MovingDeck, ("Name", "moved")), W)), Is.True);
Assert.That(a.Remove(W), Is.False);
var refusal = Assert.Throws<CultSchemaConflictException>(() => a.FlushAsync().GetAwaiter().GetResult())!;