Cacheless calls and attach share the store's gate: an attach waits for a call, a call that finds the cache is refused
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8619883b96
commit
13706c795f
2 changed files with 51 additions and 4 deletions
|
|
@ -2257,7 +2257,8 @@ namespace GameCult.Caching
|
|||
{
|
||||
if (store == null) throw new ArgumentNullException(nameof(store));
|
||||
homes ??= Array.Empty<Type>();
|
||||
Held(() =>
|
||||
// The store's own gate too: a cacheless call on it is either finished or waiting, and a waiting one finds the cache and refuses.
|
||||
Held(() => store.WhileDetached(() =>
|
||||
{
|
||||
if (store.Loaded != null)
|
||||
throw new InvalidOperationException($"Backing store {store} is already attached to a cache.");
|
||||
|
|
@ -2302,7 +2303,7 @@ namespace GameCult.Caching
|
|||
throw;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public Task PullAllBackingStoresAsync()
|
||||
|
|
@ -3591,6 +3592,13 @@ namespace GameCult.Caching
|
|||
|
||||
protected void Held(Action body) => Held(() => { body(); return true; });
|
||||
|
||||
// The store's cacheless gate, taken by the attach so that no cacheless call is mid-flight while the store gains a cache.
|
||||
internal T WhileDetached<T>(Func<T> body)
|
||||
{
|
||||
lock (_detachedGate)
|
||||
return body();
|
||||
}
|
||||
|
||||
private protected void UseRegistry(CultDocumentRegistry? registry) => _registry = registry;
|
||||
|
||||
internal void AttachRegistry(CultDocumentRegistry registry)
|
||||
|
|
@ -4237,7 +4245,6 @@ namespace GameCult.Caching
|
|||
{
|
||||
if (record == null) throw new ArgumentNullException(nameof(record));
|
||||
if (catalogEntry == null) throw new ArgumentNullException(nameof(catalogEntry));
|
||||
RefuseWhenAttached();
|
||||
ForTheCall(registry, () => WriteOne(record, catalogEntry, holdsIds));
|
||||
}
|
||||
|
||||
|
|
@ -4261,7 +4268,6 @@ namespace GameCult.Caching
|
|||
where TDocument : class
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
RefuseWhenAttached();
|
||||
var resolvedRegistry = registry ?? CultDocumentRegistry.Shared;
|
||||
var catalogEntry = resolvedRegistry.GetRequired<TDocument>().ToCatalogEntry();
|
||||
ForTheCall(resolvedRegistry, () =>
|
||||
|
|
@ -4294,6 +4300,7 @@ namespace GameCult.Caching
|
|||
private void ForTheCall(CultDocumentRegistry? registry, Action body) =>
|
||||
Held(() =>
|
||||
{
|
||||
RefuseWhenAttached();
|
||||
UseRegistry(registry);
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -163,6 +163,46 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That(new SingleFileMessagePackBackingStore(_path).ReadStoreSnapshot()!.Records.Select(record => record.Key), Is.EquivalentTo(new[] { "a", "first", "second" }));
|
||||
}
|
||||
|
||||
// An attach during a cacheless call waits for it: the call finishes on the registry it was given, the store then gains the
|
||||
// cache's registry, and the cache's next flush works.
|
||||
[Test]
|
||||
public async Task AnAttachDuringACachelessCallWaitsForIt()
|
||||
{
|
||||
var mine = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) });
|
||||
var cacheRegistry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) });
|
||||
new SingleFileMessagePackBackingStore(_path).WriteDocument(new CultRecordKey("a"), new IdDeck { Name = "a" }, registry: mine);
|
||||
var store = new GatedStore(_path);
|
||||
|
||||
var write = Task.Run(() => store.WriteDocument(new CultRecordKey("first"), new IdDeck { Name = "first" }, registry: mine));
|
||||
Assert.That(store.Entered.Wait(TimeSpan.FromSeconds(10)), Is.True, "the call reached the reader");
|
||||
using var cache = new CultCache(cacheRegistry);
|
||||
var attach = Task.Run(() => cache.AddBackingStore(store));
|
||||
Assert.That(attach.Wait(TimeSpan.FromMilliseconds(500)), Is.False, "the attach waits while a cacheless call is inside the store");
|
||||
store.Release.Set();
|
||||
await write;
|
||||
await attach;
|
||||
|
||||
Assert.That(store.RegistryAfterWait, Is.SameAs(mine), "the call kept its own registry to its end");
|
||||
Assert.That(store.Attached, Is.SameAs(cacheRegistry), "the call's end did not detach the cache's registry");
|
||||
await cache.AddAsync(new IdDeck { Name = "b" });
|
||||
Assert.DoesNotThrow(() => cache.FlushAllBackingStores());
|
||||
}
|
||||
|
||||
// A cacheless call that finds the store attached by the time it holds the gate is refused, and leaves the cache's registry.
|
||||
[Test]
|
||||
public void ACachelessCallOnAnAttachedStoreIsRefusedAndLeavesTheRegistry()
|
||||
{
|
||||
var cacheRegistry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) });
|
||||
var store = new GatedStore(_path);
|
||||
store.Release.Set();
|
||||
using var cache = new CultCache(cacheRegistry);
|
||||
cache.AddBackingStore(store);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => store.WriteDocument(new CultRecordKey("x"), new IdDeck { Name = "x" }));
|
||||
Assert.Throws<InvalidOperationException>(() => store.WriteRecord(Record("a", One, 1), One));
|
||||
Assert.That(store.Attached, Is.SameAs(cacheRegistry));
|
||||
}
|
||||
|
||||
// WriteDocument<T> stores the document as T: the schema id, the id plan, the header and the bytes all come from the one
|
||||
// declared type, as they did when CultMesh encoded typeof(T).
|
||||
[Test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue