One owner for the foreign-record refusal: it reaches the cache, names every key, and refuses only what the cache wrote

CacheBackingStore.RefuseForeign now decides, for both store kinds, which written keys a foreign record refuses, drops the
store's staged change and the cache's copy of each through the Loaded path a pull uses, and throws one conflict naming every
refused key. The single-file store tracks the keys staged since its last load or write, as the directory store already did, so
a clean copy of a record another writer made foreign is carried, not refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-02 18:34:09 +02:00
commit 86eea40bce
3 changed files with 178 additions and 47 deletions

View file

@ -243,28 +243,19 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
}
// The manifest's other records stay as they are, foreign ones included; a write that would replace or remove a foreign
// record is refused before any page is written. The refusal takes the refused keys with it: a staged change to a record that
// is foreign now, and this store's copy of one, are not what the file holds, and kept they would refuse every later write.
private void RefuseForeign(CultPersistedStoreSnapshot currentManifest, IEnumerable<string> written)
{
var foreign = currentManifest.Records
.Where(record => Foreign(record, currentManifest.SchemaCatalog) != null)
.Select(record => record.Key)
.ToHashSet(StringComparer.Ordinal);
var refused = written.Concat(_deletedKeys.Keys).Concat(_dirtyKeys.Keys).Where(foreign.Contains).Distinct().ToArray();
if (refused.Length == 0)
return;
foreach (var key in refused)
{
Entries.TryRemove(key, out _);
_dirtyKeys.TryRemove(key, out _);
_deletedKeys.TryRemove(key, out _);
}
IsDirty = !_dirtyKeys.IsEmpty || !_deletedKeys.IsEmpty;
var held = currentManifest.Records.First(record => record.Key == refused[0]);
throw Overwrites(Foreign(held, currentManifest.SchemaCatalog)!);
}
// record is refused before any page is written, by the one rule every store shares (CacheBackingStore.RefuseForeign).
private void RefuseForeign(CultPersistedStoreSnapshot currentManifest, IEnumerable<string> written) =>
RefuseForeign(
written.Concat(_deletedKeys.Keys).Concat(_dirtyKeys.Keys),
key => currentManifest.Records.FirstOrDefault(record => record.Key == key) is { } held
? Foreign(held, currentManifest.SchemaCatalog)
: null,
key =>
{
_dirtyKeys.TryRemove(key, out _);
_deletedKeys.TryRemove(key, out _);
IsDirty = !_dirtyKeys.IsEmpty || !_deletedKeys.IsEmpty;
});
// Runs under the commit lease: pages first, then the manifest that names them.
private void WriteGeneration(CultPersistedStoreSnapshot currentManifest)