Share the cache gate with its stores and make unconditional commit last-writer-wins
An attached store takes its cache's gate as its own lock (CacheBackingStore.Gate), so a pull's load callback into the cache can no longer take the gate after the store lock. The directory store's private mutation gate is deleted; the single-file pull now runs under the gate; PullAllBackingStoresAsync takes the gate around each store's pull. Fixes the pull/upsert deadlock on directory stores and the re-pull that erased a staged single-file write. An unconditional single-file Commit writes the store's whole view plus the batch, exactly as a flush would, instead of merging onto the file; it persists earlier staged writes and leaves the store clean. Conditional commits still land onto the file under the lock. Directory-store commit and flush already share one generation writer (staged keys onto the current manifest) and are unchanged. Contracts state both rules, the lock order, that OnUpdate fires for loads only, that a missing single file keeps staged writes, and that a store failing to hydrate on open throws and is left byte-identical. Tests: PullAndUpsertOnDirectoryStoreDoNotDeadlock, RepullNeverErasesAStagedWrite, UnconditionalCommitIsLastWriterWinsLikeFlush, UnconditionalCommitPersistsStagedWrites, OpeningACorruptStoreThrowsAndLeavesItUntouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2a44209510
commit
5aad137e61
5 changed files with 236 additions and 25 deletions
|
|
@ -17,7 +17,6 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
private const string IndexedFormatVersion = "cultcache.store.v4.directory-content-addressed-pages";
|
||||
private readonly FileInfo _manifestFile;
|
||||
private readonly DirectoryInfo _recordDirectory;
|
||||
private readonly object _mutationGate = new();
|
||||
private readonly ConcurrentDictionary<string, bool> _dirtyKeys = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, bool> _deletedKeys = new(StringComparer.Ordinal);
|
||||
private readonly HashSet<string> _hydratedKeys = new(StringComparer.Ordinal);
|
||||
|
|
@ -42,7 +41,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
|
||||
public override void PullAll()
|
||||
{
|
||||
lock (_mutationGate)
|
||||
lock (Gate)
|
||||
PullAllCore();
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +111,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
public override void Push(CultStoredDocument entry)
|
||||
{
|
||||
ThrowIfReadOnly();
|
||||
lock (_mutationGate)
|
||||
lock (Gate)
|
||||
{
|
||||
Entries[entry.Key.Value] = entry;
|
||||
_dirtyKeys[entry.Key.Value] = true;
|
||||
|
|
@ -124,7 +123,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
public override void Delete(CultStoredDocument entry)
|
||||
{
|
||||
ThrowIfReadOnly();
|
||||
lock (_mutationGate)
|
||||
lock (Gate)
|
||||
{
|
||||
Entries.TryRemove(entry.Key.Value, out _);
|
||||
_dirtyKeys.TryRemove(entry.Key.Value, out _);
|
||||
|
|
@ -136,7 +135,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
public override CultCommitOutcome CommitBatch(CultCommitRequest request, bool wait)
|
||||
{
|
||||
ThrowIfReadOnly();
|
||||
lock (_mutationGate)
|
||||
lock (Gate)
|
||||
{
|
||||
Directory.CreateDirectory(_manifestFile.DirectoryName!);
|
||||
using var commitLease = AcquireCommitLease(wait);
|
||||
|
|
@ -188,7 +187,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
public override void PushAll()
|
||||
{
|
||||
ThrowIfReadOnly();
|
||||
lock (_mutationGate)
|
||||
lock (Gate)
|
||||
{
|
||||
if (!IsDirty)
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue