Removing a foreign record is judged against the store as it is, not the last load

A removal of a key the cache does not hold writes nothing, so it never reached
the apply; it was checked against the foreign list of the last load, and a
foreign record that arrived since was reported removed. Each store now reads
its durable state for such a removal (RefuseRemovalOfUndeclared) and refuses
through the apply's own RefuseUndeclaredOnDisk, which reloads and lists the
record. The load-time check (RefuseRemovalOfForeign) is deleted: the durable
read refuses everything it refused, and it could not save that read. A store
kind that cannot read its durable state keeps the view check as its default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-03 15:13:32 +02:00
commit 43a63b0792
3 changed files with 57 additions and 16 deletions

View file

@ -185,7 +185,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
manifest.Records.ToDictionary(record => record.Key, record => VersionOf(record.Payload), StringComparer.Ordinal), Entries.Values))
return CultCommitOutcome.Mismatch;
RefuseUndeclaredOnDisk(manifest, request.Upserts.Concat(request.Deletes).Select(entry => entry.Key.Value));
RefuseUndeclaredOnDisk(manifest, request.Upserts.Concat(request.Deletes).Select(entry => entry.Key.Value).Concat(StagedKeys));
var previousEntries = Entries.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
var previousDirtyKeys = _dirtyKeys.Keys.ToArray();
var previousDeletedKeys = _deletedKeys.Keys.ToArray();
@ -236,11 +236,18 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
Directory.CreateDirectory(_manifestFile.DirectoryName!);
using var commitLease = AcquireCommitLease(wait: true, create: true);
var (manifest, catalogBytes) = ReadManifest();
RefuseUndeclaredOnDisk(manifest, Array.Empty<string>());
RefuseUndeclaredOnDisk(manifest, StagedKeys);
WriteGeneration(manifest, catalogBytes);
});
}
private IEnumerable<string> StagedKeys => _deletedKeys.Keys.Concat(_dirtyKeys.Keys);
// The manifest as it is now, read without the commit lease: nothing is written after the read, the manifest is only ever
// replaced whole, and a read-only store must not create the lease.
protected internal override void RefuseRemovalOfUndeclared(IReadOnlyCollection<string> keys) =>
Held(() => RefuseUndeclaredOnDisk(ReadManifest().Snapshot, keys));
// The manifest's other records stay as they are, foreign and read-only ones included; a write that would replace or remove a
// record the manifest holds under an id no registered type declares is refused before any page is written, by the one rule every
// store shares (CacheBackingStore.RefuseUndeclaredOnDisk). The refused records reload as a pull loads them.
@ -249,7 +256,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
CultPersistedRecord? HeldAt(string key) =>
currentManifest.Records.FirstOrDefault(record => string.Equals(record.Key, key, StringComparison.Ordinal));
RefuseUndeclaredOnDisk(
written.Concat(_deletedKeys.Keys).Concat(_dirtyKeys.Keys),
written,
key => HeldAt(key) is { } held ? Undeclared(held, currentManifest.SchemaCatalog, Registry.Declares) : null,
key => HeldAt(key) is { } held && Foreign(held, currentManifest.SchemaCatalog) == null
? LoadPage(held, currentManifest.SchemaCatalog, out _, out _)