Directory store: a refused foreign overwrite leaves the pending set with its failed write
WriteGeneration left the refused key in the dirty and deleted sets, so every later generation refused it. The refusal is now one check before any staging, shared by flush and commit, that removes the refused keys from the pending sets. Tests cover both store kinds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c246e0c5ae
commit
b0f7d59c96
2 changed files with 66 additions and 9 deletions
|
|
@ -186,6 +186,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
if (!request.ConditionsHold(manifest.Records, Entries.Values))
|
||||
return CultCommitOutcome.Mismatch;
|
||||
|
||||
RefuseForeign(manifest, request.Upserts.Concat(request.Deletes).Select(entry => entry.Key.Value));
|
||||
var previousEntries = Entries.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||
var previousDirtyKeys = _dirtyKeys.Keys.ToArray();
|
||||
var previousDeletedKeys = _deletedKeys.Keys.ToArray();
|
||||
|
|
@ -235,23 +236,41 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
|
||||
Directory.CreateDirectory(_manifestFile.DirectoryName!);
|
||||
using var commitLease = AcquireCommitLease(wait: true, create: true);
|
||||
WriteGeneration(ReadManifest());
|
||||
var manifest = ReadManifest();
|
||||
RefuseForeign(manifest, Array.Empty<string>());
|
||||
WriteGeneration(manifest);
|
||||
});
|
||||
}
|
||||
|
||||
// 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)!);
|
||||
}
|
||||
|
||||
// Runs under the commit lease: pages first, then the manifest that names them.
|
||||
private void WriteGeneration(CultPersistedStoreSnapshot currentManifest)
|
||||
{
|
||||
Directory.CreateDirectory(_recordDirectory.FullName);
|
||||
var currentIndex = currentManifest.Records.ToDictionary(record => record.Key, record => record, StringComparer.Ordinal);
|
||||
// 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.
|
||||
foreach (var key in _deletedKeys.Keys.Concat(_dirtyKeys.Keys))
|
||||
{
|
||||
if (currentIndex.TryGetValue(key, out var held) && Foreign(held, currentManifest.SchemaCatalog) is { } carried)
|
||||
throw Overwrites(carried);
|
||||
}
|
||||
|
||||
foreach (var key in _deletedKeys.Keys)
|
||||
currentIndex.Remove(key);
|
||||
foreach (var key in _dirtyKeys.Keys)
|
||||
|
|
|
|||
Loading…
Reference in a new issue