Hydrate durable globals before defaults
This commit is contained in:
parent
f714ceee43
commit
d69fa61412
4 changed files with 125 additions and 19 deletions
|
|
@ -70,15 +70,39 @@ namespace GameCult.Caching.MessagePack
|
|||
/// Creates a cache with the canonical single-file MessagePack backing store attached.
|
||||
/// </summary>
|
||||
public static CultCache Create(string filePath, CultCacheOpenOptions? options = null)
|
||||
{
|
||||
options ??= new CultCacheOpenOptions();
|
||||
return CreateCore(filePath, options, initializeGlobals: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache with the canonical single-file MessagePack backing store attached and optionally pulls the on-disk snapshot.
|
||||
/// </summary>
|
||||
public static async Task<CultCache> OpenAsync(string filePath, CultCacheOpenOptions? options = null)
|
||||
{
|
||||
options ??= new CultCacheOpenOptions();
|
||||
var cache = CreateCore(filePath, options, initializeGlobals: false);
|
||||
if (options.PullOnOpen)
|
||||
{
|
||||
await cache.PullAllBackingStoresAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
cache.MaterializeMissingGlobals();
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
private static CultCache CreateCore(
|
||||
string filePath,
|
||||
CultCacheOpenOptions options,
|
||||
bool initializeGlobals)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
throw new ArgumentException("File path must be non-empty.", nameof(filePath));
|
||||
}
|
||||
|
||||
options ??= new CultCacheOpenOptions();
|
||||
|
||||
var cache = new CultCache(options.Registry)
|
||||
var cache = new CultCache(options.Registry, initializeGlobals)
|
||||
{
|
||||
FlushAttachedStoresOnDispose = options.FlushOnDispose
|
||||
};
|
||||
|
|
@ -106,20 +130,5 @@ namespace GameCult.Caching.MessagePack
|
|||
|
||||
return cache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache with the canonical single-file MessagePack backing store attached and optionally pulls the on-disk snapshot.
|
||||
/// </summary>
|
||||
public static async Task<CultCache> OpenAsync(string filePath, CultCacheOpenOptions? options = null)
|
||||
{
|
||||
options ??= new CultCacheOpenOptions();
|
||||
var cache = Create(filePath, options);
|
||||
if (options.PullOnOpen)
|
||||
{
|
||||
await cache.PullAllBackingStoresAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,12 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
IsDirty = !_dirtyKeys.IsEmpty || !_deletedKeys.IsEmpty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool ContainsDurableRecord(CultRecordKey key)
|
||||
{
|
||||
return _durableIndex.ContainsKey(key.Value) || base.ContainsDurableRecord(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Push(CultStoredDocument entry)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1284,8 +1284,29 @@ namespace GameCult.Caching
|
|||
/// Creates a cache using the supplied document registry or the shared registry.
|
||||
/// </summary>
|
||||
public CultCache(CultDocumentRegistry? registry = null)
|
||||
: this(registry, initializeGlobals: true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache using the supplied document registry, optionally deferring global defaults
|
||||
/// until a durable backing store has been hydrated.
|
||||
/// </summary>
|
||||
public CultCache(CultDocumentRegistry? registry, bool initializeGlobals)
|
||||
{
|
||||
_registry = registry ?? CultDocumentRegistry.Shared;
|
||||
if (initializeGlobals)
|
||||
{
|
||||
MaterializeMissingGlobals();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates default instances for global document types that do not already have a record.
|
||||
/// Durable open paths call this after hydration so persisted globals remain authoritative.
|
||||
/// </summary>
|
||||
public void MaterializeMissingGlobals()
|
||||
{
|
||||
InitializeGlobals();
|
||||
}
|
||||
|
||||
|
|
@ -1800,6 +1821,13 @@ namespace GameCult.Caching
|
|||
{
|
||||
foreach (var descriptor in _registry.AllDescriptors.Where(candidate => candidate.IsGlobal))
|
||||
{
|
||||
var key = new CultRecordKey($"global:{descriptor.SchemaId}");
|
||||
if (_entries.ContainsKey(key.Value) ||
|
||||
_backingStores.Any(store => store.ContainsDurableRecord(key)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (descriptor.DocumentType.GetConstructor(Type.EmptyTypes) == null)
|
||||
{
|
||||
continue;
|
||||
|
|
@ -1813,7 +1841,7 @@ namespace GameCult.Caching
|
|||
|
||||
AddStoredDocumentInternal(
|
||||
new CultStoredDocument(
|
||||
new CultRecordKey($"global:{descriptor.SchemaId}"),
|
||||
key,
|
||||
DateTimeOffset.UtcNow.ToString("O"),
|
||||
descriptor,
|
||||
instance),
|
||||
|
|
@ -2037,6 +2065,13 @@ namespace GameCult.Caching
|
|||
PullAll();
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns whether the backing store knows a record is durable, including records left cold by selective hydration.
|
||||
/// </summary>
|
||||
public virtual bool ContainsDurableRecord(CultRecordKey key)
|
||||
{
|
||||
return Entries.ContainsKey(key.Value);
|
||||
}
|
||||
/// <summary>
|
||||
/// Pushes one stored document into the backing store.
|
||||
/// </summary>
|
||||
public abstract void Push(CultStoredDocument entry);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
|
@ -321,6 +322,54 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCacheMessagePack_OpenAsync_HydratesPersistedGlobalBeforeDefault()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.cc");
|
||||
var recordsPath = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath);
|
||||
var registry = CultDocumentRegistry.ForTypes(new[] { typeof(GlobalTestEntry) });
|
||||
typeof(CultDocumentDescriptor)
|
||||
.GetField("<IsGlobal>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.SetValue(registry.GetRequired<GlobalTestEntry>(), true);
|
||||
|
||||
try
|
||||
{
|
||||
using (var seed = await CultCacheMessagePack.OpenAsync(
|
||||
filePath,
|
||||
new CultCacheOpenOptions { Registry = registry, UseDirectoryStore = true }))
|
||||
{
|
||||
var global = seed.GetGlobal<GlobalTestEntry>();
|
||||
Assert.That(global, Is.Not.Null);
|
||||
global!.Value = "durable authority";
|
||||
await seed.UpsertAsync(global);
|
||||
await seed.FlushAsync();
|
||||
}
|
||||
|
||||
using var reopened = await CultCacheMessagePack.OpenAsync(
|
||||
filePath,
|
||||
new CultCacheOpenOptions
|
||||
{
|
||||
Registry = registry,
|
||||
UseDirectoryStore = true,
|
||||
DirectoryStoreHydrationFilter = _ => false
|
||||
});
|
||||
|
||||
Assert.That(reopened.GetGlobal<GlobalTestEntry>(), Is.Null,
|
||||
"the hydration filter owns whether the durable global becomes hot");
|
||||
Assert.That(reopened.IsDirty, Is.False,
|
||||
"a cold durable global must not stage its schema default as a write");
|
||||
|
||||
var globalKey = "global:" + registry.GetRequired<GlobalTestEntry>().SchemaId;
|
||||
await reopened.PullBackingStoreRecordsAsync(metadata => metadata.Key == globalKey);
|
||||
Assert.That(reopened.GetGlobal<GlobalTestEntry>()?.Value, Is.EqualTo("durable authority"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
if (Directory.Exists(recordsPath)) Directory.Delete(recordsPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DirectoryMessagePackBackingStore_CleanFlush_DoesNotRewriteManifest()
|
||||
{
|
||||
|
|
@ -989,6 +1038,13 @@ namespace GameCult.Caching.Tests
|
|||
public string Value = string.Empty;
|
||||
}
|
||||
|
||||
[CultDocument("tests.global_entry", "tests.global_entry.v1")]
|
||||
internal sealed class GlobalTestEntry
|
||||
{
|
||||
[Key(0)]
|
||||
public string Value = "schema default";
|
||||
}
|
||||
|
||||
[CultDocument("tests.reference_holder", "tests.reference_holder.v1")]
|
||||
internal sealed class ReferenceHolderEntry
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue