Delete CultCache wrappers and unused members
Removes FlushBackingStore, both PrepareForReloadOrShutdown overloads, PullBackingStoreRecordsAsync, TryGet<T>, TryGetByName, TryGetByIndex, Resolve<T>, the cache and store Logger, and LastSuccessfulFlushAtUtc on the cache and stores. GetStoredDocuments<T> folds into GetAll<T>. The soft flag goes from FlushAllBackingStores, ExecuteTransactionAsync, CommitBatch and PushAll. It stays on FlushAsync(bool) because CultCacheStudioWindow reflects on that signature until Cut 6. CultMesh.FlushAsync and CultNetLocal.FlushAsync keep their parameter and stop forwarding it. The store's EntryAdded/EntryUpdated/EntryDeleted subjects become protected internal Loaded/Unloaded callbacks set by the cache at attach. AddStoredDocumentInternal is synchronous, so the GetAwaiter().GetResult() bridges on the store subscriptions and the global materializer are gone. Tests: drop the LastSuccessfulFlushAtUtc asserts; OpenAsync and GetAll tests read through Get/GetAll; the persisted-global test proves the durable global loads without staging a default instead of going through selective hydration; the lease-before-observers test observes through CultCache.Watch; the PullSelected selective-hydration test is deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1982d73e44
commit
85f2089f0e
5 changed files with 55 additions and 316 deletions
|
|
@ -201,10 +201,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
}
|
||||
|
||||
Entries[pair.Key] = pair.Value;
|
||||
if (existing == null)
|
||||
EntryAdded.OnNext(pair.Value);
|
||||
else
|
||||
EntryUpdated.OnNext(pair.Value);
|
||||
Loaded?.Invoke(pair.Value);
|
||||
}
|
||||
|
||||
foreach (var removedKey in _hydratedKeys.Where(key => !loaded.ContainsKey(key)).ToArray())
|
||||
|
|
@ -213,7 +210,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
continue;
|
||||
|
||||
if (Entries.TryRemove(removedKey, out var removed))
|
||||
EntryDeleted.OnNext(removed);
|
||||
Unloaded?.Invoke(removed);
|
||||
_hydratedKeys.Remove(removedKey);
|
||||
}
|
||||
|
||||
|
|
@ -307,8 +304,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
/// <inheritdoc />
|
||||
public override void CommitBatch(
|
||||
IReadOnlyCollection<CultStoredDocument> upserts,
|
||||
IReadOnlyCollection<CultStoredDocument> deletes,
|
||||
bool soft)
|
||||
IReadOnlyCollection<CultStoredDocument> deletes)
|
||||
{
|
||||
lock (_mutationGate)
|
||||
{
|
||||
|
|
@ -331,7 +327,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
_deletedKeys.TryRemove(entry.Key.Value, out _);
|
||||
}
|
||||
IsDirty = true;
|
||||
PushAllCore(soft);
|
||||
PushAllCore();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -351,13 +347,13 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void PushAll(bool soft = false)
|
||||
public override void PushAll()
|
||||
{
|
||||
lock (_mutationGate)
|
||||
PushAllCore(soft);
|
||||
PushAllCore();
|
||||
}
|
||||
|
||||
private void PushAllCore(bool soft)
|
||||
private void PushAllCore()
|
||||
{
|
||||
if (!IsDirty && !_needsIndexUpgrade)
|
||||
{
|
||||
|
|
@ -652,10 +648,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
continue;
|
||||
|
||||
Entries[pair.Key] = pair.Value;
|
||||
if (existing == null)
|
||||
EntryAdded.OnNext(pair.Value);
|
||||
else
|
||||
EntryUpdated.OnNext(pair.Value);
|
||||
Loaded?.Invoke(pair.Value);
|
||||
}
|
||||
|
||||
foreach (var missingKey in selectedKeys.Where(key => !loaded.ContainsKey(key)).ToArray())
|
||||
|
|
@ -663,7 +656,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
if (_dirtyKeys.ContainsKey(missingKey) || _deletedKeys.ContainsKey(missingKey))
|
||||
continue;
|
||||
if (Entries.TryRemove(missingKey, out var removed))
|
||||
EntryDeleted.OnNext(removed);
|
||||
Unloaded?.Invoke(removed);
|
||||
_hydratedKeys.Remove(missingKey);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using System.Security.Cryptography;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Logging;
|
||||
using R3;
|
||||
|
||||
namespace GameCult.Caching
|
||||
|
|
@ -1281,7 +1280,6 @@ namespace GameCult.Caching
|
|||
private readonly SemaphoreSlim _transactionGate = new(1, 1);
|
||||
private readonly AsyncLocal<CultCacheTransaction?> _ambientTransaction = new();
|
||||
private readonly object _stateGate = new();
|
||||
private ILogger _logger = new NullLogger();
|
||||
private bool _hasUnflushedMutations;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1314,25 +1312,11 @@ namespace GameCult.Caching
|
|||
InitializeGlobals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cache logger.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get => _logger;
|
||||
set => _logger = value ?? new NullLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the cache currently holds unflushed mutations in any attached backing store or only in memory.
|
||||
/// </summary>
|
||||
public bool IsDirty => _hasUnflushedMutations || _backingStores.Any(store => store.IsDirty);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC timestamp of the last successful flush across all attached backing stores.
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastSuccessfulFlushAtUtc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether disposing the cache should flush attached dirty backing stores first.
|
||||
/// </summary>
|
||||
|
|
@ -1405,10 +1389,8 @@ namespace GameCult.Caching
|
|||
{
|
||||
if (store == null) throw new ArgumentNullException(nameof(store));
|
||||
store.AttachRegistry(_registry);
|
||||
store.Logger = Logger;
|
||||
store.EntryAdded.Subscribe(entry => AddStoredDocumentInternal(entry, store, raiseUpdate: true).GetAwaiter().GetResult());
|
||||
store.EntryUpdated.Subscribe(entry => AddStoredDocumentInternal(entry, store, raiseUpdate: true).GetAwaiter().GetResult());
|
||||
store.EntryDeleted.Subscribe(entry => RemoveStoredDocumentInternal(entry, store, raiseUpdate: true));
|
||||
store.Loaded = entry => AddStoredDocumentInternal(entry, store, raiseUpdate: true);
|
||||
store.Unloaded = entry => RemoveStoredDocumentInternal(entry, store, raiseUpdate: true);
|
||||
|
||||
foreach (var entry in _entries.Values.OrderBy(entry => entry.Key.Value, StringComparer.Ordinal))
|
||||
{
|
||||
|
|
@ -1441,35 +1423,10 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulls persisted records selected by durable metadata from every attached backing store.
|
||||
/// Stores without indexed selection support preserve correctness by performing a full pull.
|
||||
/// </summary>
|
||||
public async Task PullBackingStoreRecordsAsync(Func<CultPersistedRecordMetadata, bool> selector)
|
||||
{
|
||||
if (selector == null) throw new ArgumentNullException(nameof(selector));
|
||||
if (_ambientTransaction.Value != null)
|
||||
throw new InvalidOperationException(
|
||||
"CultCache hydration cannot run inside a mutation transaction; hydrate before opening the commit scope.");
|
||||
|
||||
await _transactionGate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
foreach (var store in _backingStores)
|
||||
store.PullSelected(selector);
|
||||
|
||||
_hasUnflushedMutations = _backingStores.Any(store => store.IsDirty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_transactionGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes all attached backing stores.
|
||||
/// </summary>
|
||||
public void FlushAllBackingStores(bool soft = false)
|
||||
public void FlushAllBackingStores()
|
||||
{
|
||||
if (_ambientTransaction.Value != null)
|
||||
throw new InvalidOperationException("A CultCache transaction owns durable commit; do not flush inside its stage callback.");
|
||||
|
|
@ -1478,11 +1435,10 @@ namespace GameCult.Caching
|
|||
try
|
||||
{
|
||||
foreach (var store in _backingStores)
|
||||
FlushBackingStoreCore(store, soft);
|
||||
|
||||
RecomputeDirtyState();
|
||||
if (!IsDirty)
|
||||
LastSuccessfulFlushAtUtc = DateTimeOffset.UtcNow;
|
||||
{
|
||||
store.PushAll();
|
||||
RecomputeDirtyState();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -1490,64 +1446,10 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes all attached backing stores.
|
||||
/// </summary>
|
||||
// soft is dead. CultCacheStudioWindow reflects on FlushAsync(bool) until Cut 6 rewrites it.
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
FlushAllBackingStores(soft);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes one attached backing store.
|
||||
/// </summary>
|
||||
public void FlushBackingStore(CacheBackingStore store, bool soft = false)
|
||||
{
|
||||
if (store == null) throw new ArgumentNullException(nameof(store));
|
||||
if (!_backingStores.Contains(store))
|
||||
{
|
||||
throw new InvalidOperationException("Backing store is not attached to this cache.");
|
||||
}
|
||||
|
||||
if (_ambientTransaction.Value != null)
|
||||
throw new InvalidOperationException("A CultCache transaction owns durable commit; do not flush inside its stage callback.");
|
||||
|
||||
_transactionGate.Wait();
|
||||
try
|
||||
{
|
||||
FlushBackingStoreCore(store, soft);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_transactionGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushBackingStoreCore(CacheBackingStore store, bool soft)
|
||||
{
|
||||
store.PushAll(soft);
|
||||
RecomputeDirtyState();
|
||||
if (!store.IsDirty)
|
||||
{
|
||||
LastSuccessfulFlushAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes attached backing stores at a lifecycle boundary such as shutdown or assembly reload.
|
||||
/// </summary>
|
||||
public void PrepareForReloadOrShutdown(bool soft = false)
|
||||
{
|
||||
FlushAllBackingStores(soft);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes attached backing stores at a lifecycle boundary such as shutdown or assembly reload.
|
||||
/// </summary>
|
||||
public Task PrepareForReloadOrShutdownAsync(bool soft = false)
|
||||
{
|
||||
PrepareForReloadOrShutdown(soft);
|
||||
FlushAllBackingStores();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
|
@ -1556,7 +1458,7 @@ namespace GameCult.Caching
|
|||
/// async flow. The durable backing store is committed before the live cache and its
|
||||
/// observers advance; an exception discards the entire staged batch.
|
||||
/// </summary>
|
||||
public async Task ExecuteTransactionAsync(Func<Task> stageAsync, bool soft = false)
|
||||
public async Task ExecuteTransactionAsync(Func<Task> stageAsync)
|
||||
{
|
||||
if (stageAsync == null) throw new ArgumentNullException(nameof(stageAsync));
|
||||
if (_ambientTransaction.Value != null)
|
||||
|
|
@ -1573,7 +1475,7 @@ namespace GameCult.Caching
|
|||
{
|
||||
await stageAsync().ConfigureAwait(false);
|
||||
transaction.Seal();
|
||||
changes = CommitTransaction(transaction, soft);
|
||||
changes = CommitTransaction(transaction);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -1587,14 +1489,14 @@ namespace GameCult.Caching
|
|||
}
|
||||
|
||||
/// <summary>Runs a value-producing buffered cache mutation.</summary>
|
||||
public async Task<T> ExecuteTransactionAsync<T>(Func<Task<T>> stageAsync, bool soft = false)
|
||||
public async Task<T> ExecuteTransactionAsync<T>(Func<Task<T>> stageAsync)
|
||||
{
|
||||
if (stageAsync == null) throw new ArgumentNullException(nameof(stageAsync));
|
||||
T result = default!;
|
||||
await ExecuteTransactionAsync(async () =>
|
||||
{
|
||||
result = await stageAsync().ConfigureAwait(false);
|
||||
}, soft).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1614,10 +1516,8 @@ namespace GameCult.Caching
|
|||
await _transactionGate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var stored = await AddStoredDocumentInternal(
|
||||
CreateStoredDocument(typeof(T), document, handle?.Key),
|
||||
source: null,
|
||||
raiseUpdate: false).ConfigureAwait(false);
|
||||
var stored = CreateStoredDocument(typeof(T), document, handle?.Key);
|
||||
AddStoredDocumentInternal(stored, source: null, raiseUpdate: false);
|
||||
return new CultRecordHandle<T>(stored.Key);
|
||||
}
|
||||
finally
|
||||
|
|
@ -1658,10 +1558,8 @@ namespace GameCult.Caching
|
|||
await _transactionGate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var stored = await AddStoredDocumentInternal(
|
||||
CreateStoredDocument(documentType, document, key),
|
||||
source: null,
|
||||
raiseUpdate: false).ConfigureAwait(false);
|
||||
var stored = CreateStoredDocument(documentType, document, key);
|
||||
AddStoredDocumentInternal(stored, source: null, raiseUpdate: false);
|
||||
return stored.Key;
|
||||
}
|
||||
finally
|
||||
|
|
@ -1705,33 +1603,17 @@ namespace GameCult.Caching
|
|||
return Get(key) as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a typed document by record key.
|
||||
/// </summary>
|
||||
public bool TryGet<T>(CultRecordKey key, out T? document) where T : class
|
||||
{
|
||||
document = Get<T>(key);
|
||||
return document != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all cached documents assignable to the requested type.
|
||||
/// </summary>
|
||||
public IEnumerable<T> GetAll<T>() where T : class
|
||||
{
|
||||
return GetStoredDocuments<T>().Select(entry => (T)entry.Document);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets typed cached records with their stable CultCache identities without scanning unrelated documents.
|
||||
/// </summary>
|
||||
public IEnumerable<CultStoredDocument> GetStoredDocuments<T>() where T : class
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
var type = typeof(T);
|
||||
return VisibleStoredDocuments()
|
||||
.Where(entry => type.IsAssignableFrom(entry.Descriptor.DocumentType))
|
||||
.Select(entry => (T)entry.Document)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -1761,15 +1643,6 @@ namespace GameCult.Caching
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a typed document by its CultName value.
|
||||
/// </summary>
|
||||
public bool TryGetByName<T>(string name, out T? document) where T : class
|
||||
{
|
||||
document = GetByName<T>(name);
|
||||
return document != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a typed document by an indexed value.
|
||||
/// </summary>
|
||||
|
|
@ -1784,23 +1657,6 @@ namespace GameCult.Caching
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a typed document by an indexed value.
|
||||
/// </summary>
|
||||
public bool TryGetByIndex<T>(string alias, string value, out T? document) where T : class
|
||||
{
|
||||
document = GetByIndex<T>(alias, value);
|
||||
return document != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a typed document reference against this cache.
|
||||
/// </summary>
|
||||
public T? Resolve<T>(CultRecordRef<T> reference) where T : class
|
||||
{
|
||||
return Get<T>(reference.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a document by typed handle.
|
||||
/// </summary>
|
||||
|
|
@ -1898,8 +1754,7 @@ namespace GameCult.Caching
|
|||
}
|
||||
|
||||
private IReadOnlyList<(CultStoredDocument Stored, object? Previous, bool Removed)> CommitTransaction(
|
||||
CultCacheTransaction transaction,
|
||||
bool soft)
|
||||
CultCacheTransaction transaction)
|
||||
{
|
||||
var mutations = transaction.SnapshotForCommit()
|
||||
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
|
||||
|
|
@ -1932,7 +1787,7 @@ namespace GameCult.Caching
|
|||
.ToArray();
|
||||
|
||||
foreach (var store in _backingStores)
|
||||
store.CommitBatch(upserts, deleted, soft);
|
||||
store.CommitBatch(upserts, deleted);
|
||||
|
||||
var changes = new List<(CultStoredDocument Stored, object? Previous, bool Removed)>();
|
||||
lock (_stateGate)
|
||||
|
|
@ -1964,8 +1819,6 @@ namespace GameCult.Caching
|
|||
changes.Add((stored, existing?.Document, false));
|
||||
}
|
||||
_hasUnflushedMutations = _backingStores.Any(store => store.IsDirty);
|
||||
if (!IsDirty)
|
||||
LastSuccessfulFlushAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
return changes;
|
||||
|
|
@ -2051,7 +1904,7 @@ namespace GameCult.Caching
|
|||
document);
|
||||
}
|
||||
|
||||
private async Task<CultStoredDocument> AddStoredDocumentInternal(
|
||||
private void AddStoredDocumentInternal(
|
||||
CultStoredDocument stored,
|
||||
CacheBackingStore? source,
|
||||
bool raiseUpdate)
|
||||
|
|
@ -2086,9 +1939,6 @@ namespace GameCult.Caching
|
|||
{
|
||||
OnUpdate?.Invoke(existing?.Document, stored.Document);
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
return stored;
|
||||
}
|
||||
|
||||
private void RemoveStoredDocumentInternal(
|
||||
|
|
@ -2154,7 +2004,7 @@ namespace GameCult.Caching
|
|||
descriptor,
|
||||
instance),
|
||||
source: null,
|
||||
raiseUpdate: false).GetAwaiter().GetResult();
|
||||
raiseUpdate: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2301,7 +2151,6 @@ namespace GameCult.Caching
|
|||
public abstract class CacheBackingStore : IDisposable
|
||||
{
|
||||
private CultDocumentRegistry? _registry;
|
||||
private ILogger _logger = new NullLogger();
|
||||
private bool _isDirty;
|
||||
private CultSchemaMigrationReport[] _lastSchemaMigrationReports = Array.Empty<CultSchemaMigrationReport>();
|
||||
|
||||
|
|
@ -2317,15 +2166,6 @@ namespace GameCult.Caching
|
|||
protected ConcurrentDictionary<string, CultStoredDocument> Entries { get; } =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backing store logger.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get => _logger;
|
||||
set => _logger = value ?? new NullLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the backing store holds staged mutations not yet durably flushed.
|
||||
/// </summary>
|
||||
|
|
@ -2335,11 +2175,6 @@ namespace GameCult.Caching
|
|||
protected set => _isDirty = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC timestamp of the last successful durable flush.
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastSuccessfulFlushAtUtc { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether disposing the backing store should flush staged mutations first.
|
||||
/// </summary>
|
||||
|
|
@ -2350,18 +2185,9 @@ namespace GameCult.Caching
|
|||
/// </summary>
|
||||
public IReadOnlyList<CultSchemaMigrationReport> LastSchemaMigrationReports => _lastSchemaMigrationReports;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes documents added by the backing store.
|
||||
/// </summary>
|
||||
public Subject<CultStoredDocument> EntryAdded { get; } = new();
|
||||
/// <summary>
|
||||
/// Publishes documents updated by the backing store.
|
||||
/// </summary>
|
||||
public Subject<CultStoredDocument> EntryUpdated { get; } = new();
|
||||
/// <summary>
|
||||
/// Publishes documents deleted by the backing store.
|
||||
/// </summary>
|
||||
public Subject<CultStoredDocument> EntryDeleted { get; } = new();
|
||||
// Set by the cache at attach; a store calls them for records it loads or drops.
|
||||
protected internal Action<CultStoredDocument>? Loaded;
|
||||
protected internal Action<CultStoredDocument>? Unloaded;
|
||||
|
||||
internal void AttachRegistry(CultDocumentRegistry registry)
|
||||
{
|
||||
|
|
@ -2401,8 +2227,7 @@ namespace GameCult.Caching
|
|||
/// </summary>
|
||||
public virtual void CommitBatch(
|
||||
IReadOnlyCollection<CultStoredDocument> upserts,
|
||||
IReadOnlyCollection<CultStoredDocument> deletes,
|
||||
bool soft)
|
||||
IReadOnlyCollection<CultStoredDocument> deletes)
|
||||
{
|
||||
var previousEntries = Entries.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||
var wasDirty = IsDirty;
|
||||
|
|
@ -2412,7 +2237,7 @@ namespace GameCult.Caching
|
|||
Delete(entry);
|
||||
foreach (var entry in upserts)
|
||||
Push(entry);
|
||||
PushAll(soft);
|
||||
PushAll();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -2426,9 +2251,9 @@ namespace GameCult.Caching
|
|||
/// <summary>
|
||||
/// Persists all current backing store entries.
|
||||
/// </summary>
|
||||
public abstract void PushAll(bool soft = false);
|
||||
public abstract void PushAll();
|
||||
/// <summary>
|
||||
/// Releases backing store event subjects.
|
||||
/// Flushes staged mutations first when FlushOnDispose is set.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
|
|
@ -2436,10 +2261,6 @@ namespace GameCult.Caching
|
|||
{
|
||||
PushAll();
|
||||
}
|
||||
|
||||
EntryAdded.Dispose();
|
||||
EntryUpdated.Dispose();
|
||||
EntryDeleted.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2487,7 +2308,6 @@ namespace GameCult.Caching
|
|||
protected void MarkFlushSucceeded()
|
||||
{
|
||||
IsDirty = false;
|
||||
LastSuccessfulFlushAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2571,16 +2391,13 @@ namespace GameCult.Caching
|
|||
continue;
|
||||
}
|
||||
Entries[stored.Key.Value] = stored;
|
||||
if (existing == null)
|
||||
EntryAdded.OnNext(stored);
|
||||
else
|
||||
EntryUpdated.OnNext(stored);
|
||||
Loaded?.Invoke(stored);
|
||||
}
|
||||
|
||||
foreach (var removedKey in Entries.Keys.Where(key => !persistedKeys.Contains(key)).ToArray())
|
||||
{
|
||||
if (Entries.TryRemove(removedKey, out var removed))
|
||||
EntryDeleted.OnNext(removed);
|
||||
Unloaded?.Invoke(removed);
|
||||
}
|
||||
|
||||
SetLastSchemaMigrationReports(reports);
|
||||
|
|
@ -2608,7 +2425,7 @@ namespace GameCult.Caching
|
|||
/// <summary>
|
||||
/// Writes the staged snapshot to disk.
|
||||
/// </summary>
|
||||
public override void PushAll(bool soft = false)
|
||||
public override void PushAll()
|
||||
{
|
||||
var snapshot = new CultPersistedStoreSnapshot
|
||||
{
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ namespace GameCult.Mesh
|
|||
/// </summary>
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
return _host.FlushAsync(soft);
|
||||
return _host.FlushAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
return Cache.FlushAsync(soft);
|
||||
return Cache.FlushAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -178,14 +178,11 @@ namespace GameCult.Caching.Tests
|
|||
|
||||
Assert.That(cache.IsDirty, Is.True);
|
||||
Assert.That(store.IsDirty, Is.True);
|
||||
Assert.That(cache.LastSuccessfulFlushAtUtc, Is.Null);
|
||||
|
||||
cache.FlushAllBackingStores();
|
||||
|
||||
Assert.That(cache.IsDirty, Is.False);
|
||||
Assert.That(store.IsDirty, Is.False);
|
||||
Assert.That(cache.LastSuccessfulFlushAtUtc, Is.Not.Null);
|
||||
Assert.That(store.LastSuccessfulFlushAtUtc, Is.Not.Null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -213,8 +210,7 @@ namespace GameCult.Caching.Tests
|
|||
cache.Dispose();
|
||||
|
||||
var reopened = await CultCacheMessagePack.OpenAsync(filePath);
|
||||
Assert.That(reopened.TryGet(handle.Key, out NamedTestEntry? loaded), Is.True);
|
||||
Assert.That(loaded!.Value, Is.EqualTo("magic"));
|
||||
Assert.That(reopened.Get<NamedTestEntry>(handle.Key)?.Value, Is.EqualTo("magic"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -310,21 +306,11 @@ namespace GameCult.Caching.Tests
|
|||
|
||||
using var reopened = await CultCacheMessagePack.OpenAsync(
|
||||
filePath,
|
||||
new CultCacheOpenOptions
|
||||
{
|
||||
Registry = registry,
|
||||
UseDirectoryStore = true,
|
||||
DirectoryStoreHydrationFilter = _ => false
|
||||
});
|
||||
new CultCacheOpenOptions { Registry = registry, UseDirectoryStore = true });
|
||||
|
||||
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"));
|
||||
Assert.That(reopened.IsDirty, Is.False,
|
||||
"a hydrated durable global must not stage its schema default as a write");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -444,63 +430,6 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DirectoryMessagePackBackingStore_PullSelected_HydratesMatchingColdPagesOnly()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.cc");
|
||||
var recordsPath = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath);
|
||||
|
||||
try
|
||||
{
|
||||
CultRecordHandle<NamedTestEntry> hot;
|
||||
CultRecordHandle<NamedTestEntry> requested;
|
||||
CultRecordHandle<NamedTestEntry> unrelated;
|
||||
using (var seed = await CultCacheMessagePack.OpenAsync(
|
||||
filePath,
|
||||
new CultCacheOpenOptions { UseDirectoryStore = true }))
|
||||
{
|
||||
hot = await seed.UpsertAsync(new NamedTestEntry { Name = "hot", Value = "already-loaded" });
|
||||
requested = await seed.UpsertAsync(new NamedTestEntry { Name = "requested", Value = "load-later" });
|
||||
unrelated = await seed.UpsertAsync(new NamedTestEntry
|
||||
{
|
||||
Name = "unrelated",
|
||||
Value = new string('u', 1024 * 1024)
|
||||
});
|
||||
await seed.FlushAsync();
|
||||
}
|
||||
|
||||
string PageFor(CultRecordHandle<NamedTestEntry> handle) =>
|
||||
Directory.GetFiles(recordsPath, "*.msgpack").Single(path => string.Equals(
|
||||
CultDocumentMessagePackSerialization.DeserializePersistedRecord(File.ReadAllBytes(path)).Key,
|
||||
handle.Key.Value,
|
||||
StringComparison.Ordinal));
|
||||
|
||||
using var selected = await CultCacheMessagePack.OpenAsync(
|
||||
filePath,
|
||||
new CultCacheOpenOptions
|
||||
{
|
||||
UseDirectoryStore = true,
|
||||
DirectoryStoreHydrationFilter = metadata => metadata.Key == hot.Key.Value
|
||||
});
|
||||
Assert.That(selected.Get<NamedTestEntry>(requested.Key), Is.Null);
|
||||
|
||||
using (new FileStream(PageFor(unrelated), FileMode.Open, FileAccess.Read, FileShare.None))
|
||||
{
|
||||
await selected.PullBackingStoreRecordsAsync(metadata => metadata.Key == requested.Key.Value);
|
||||
Assert.That(selected.Get<NamedTestEntry>(requested.Key)?.Value, Is.EqualTo("load-later"));
|
||||
Assert.That(selected.Get<NamedTestEntry>(hot.Key)?.Value, Is.EqualTo("already-loaded"));
|
||||
Assert.That(selected.Get<NamedTestEntry>(unrelated.Key), Is.Null);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
if (Directory.Exists(recordsPath))
|
||||
Directory.Delete(recordsPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DirectoryMessagePackBackingStore_Loads_Record_When_Manifest_Misses_Catalog_Entry()
|
||||
{
|
||||
|
|
@ -876,7 +805,7 @@ namespace GameCult.Caching.Tests
|
|||
var readerCache = new CultCache();
|
||||
readerCache.AddBackingStore(readerStore);
|
||||
var observerRan = false;
|
||||
using var subscription = readerStore.EntryAdded.Subscribe(_ =>
|
||||
using var subscription = readerCache.Watch<NamedTestEntry>().Subscribe(_ =>
|
||||
{
|
||||
if (observerRan)
|
||||
return;
|
||||
|
|
@ -1059,7 +988,7 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_TypedStoredDocumentIndex_Tracks_Upsert_Replacement_And_Removal()
|
||||
public async Task CultCache_GetAll_Tracks_Upsert_Replacement_And_Removal()
|
||||
{
|
||||
var cache = new CultCache();
|
||||
var key = new CultRecordKey("typed-index:named");
|
||||
|
|
@ -1073,20 +1002,20 @@ namespace GameCult.Caching.Tests
|
|||
Name = "unrelated"
|
||||
}, new CultRecordKey("typed-index:unrelated"));
|
||||
|
||||
var initial = cache.GetStoredDocuments<NamedTestEntry>().Single();
|
||||
Assert.That(initial.Key, Is.EqualTo(key));
|
||||
Assert.That(((NamedTestEntry)initial.Document).Value, Is.EqualTo("first"));
|
||||
var initial = cache.GetAll<NamedTestEntry>().Single();
|
||||
Assert.That(cache.TryGetHandle(initial)?.Key, Is.EqualTo(key));
|
||||
Assert.That(initial.Value, Is.EqualTo("first"));
|
||||
|
||||
await cache.UpsertAsync(typeof(NamedTestEntry), new NamedTestEntry
|
||||
{
|
||||
Name = "indexed",
|
||||
Value = "second"
|
||||
}, key);
|
||||
var replaced = cache.GetStoredDocuments<NamedTestEntry>().Single();
|
||||
Assert.That(((NamedTestEntry)replaced.Document).Value, Is.EqualTo("second"));
|
||||
var replaced = cache.GetAll<NamedTestEntry>().Single();
|
||||
Assert.That(replaced.Value, Is.EqualTo("second"));
|
||||
|
||||
Assert.That(cache.Remove(key), Is.True);
|
||||
Assert.That(cache.GetStoredDocuments<NamedTestEntry>(), Is.Empty);
|
||||
Assert.That(cache.GetAll<NamedTestEntry>(), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue