Route each document type to one home store; attach hydrates; commit explicit batches
Cut 3 of the CultCache migration (Aetheria docs/cultcache-migration-cut.md, 2.1 and 2.2). One commit, because routing, the batch that replaces the ambient transaction, and conditional commit share one admission path. Deleted first: - globals: InitializeGlobals, MaterializeMissingGlobals, the two-argument constructor, the initializeGlobals plumbing in CultCacheMessagePack, ContainsDurableRecord (cache and directory store) - replication: the push-to-every-store loops and the attach-time push of existing records - typeof(T) as the schema owner in AddAsync/UpsertAsync and in CultNetDatabase.PutAsync/PutPredictedAsync - the ambient transaction: ExecuteTransactionAsync x2, CultCacheTransaction, CommitTransaction, VisibleStoredDocuments, the AsyncLocal, the SemaphoreSlim gate, and every ambient check - CultCacheOpenOptions.PullOnOpen; OpenAsync is Task.FromResult(Create(...)) - DirectoryMessagePackBackingStore.ResolveLegacyUncataloguedRecordCatalog: the catalog precommit (fe55bff) is an ancestor of the v4 page format (eea8248), so no v4 manifest can reach it - CultCache.DeleteAsync<T>; its one caller, CultNetDatabaseSubscriptionClient, called it by reflection and now calls Remove(key) Added: - AddBackingStore(store, params Type[] homes): attach validates (clean, not already attached, one untyped store, no claimed home type, no admitted record changes home) and then pulls under the gate - Home(type): most derived routed home, else the untyped store - Admit: every add and remove, from a write, a batch or a load, validates (home, read-only, loaded-from-home, global singleton), lands durably, then applies and publishes; a pull hands its whole delta over in one call, so a refused record leaves the store and the cache unchanged - read-only stores (CacheBackingStore(readOnly), both store constructors, CultCacheOpenOptions.ReadOnly); flush skips them and writes only dirty stores - assignable GetGlobal<T>/GetByName<T>/GetByIndex<T>/Watch<T>; more than one candidate throws naming them - Commit(Action<CultCacheBatch>) returning bool, TryCommit returning CultCommitOutcome, CultCacheBatch (Upsert, Remove, Expect, ExpectUnchanged, sealed after commit), CultCommitRequest; CacheBackingStore.CommitBatch evaluates conditions under the store's exclusive lock - single-file store: <path>.lock sidecar for CommitBatch and PushAll; a clean store commits onto the file as it is, so records it never loaded survive - storedAt minted in one place and bumped a tick when not later than the record it replaces Consumer adjustments in this repo: Mesh streaming tests drop PullOnOpen; the Studio bridge stops reflecting on it; BackingStoreTests' global test creates its global explicitly and its transaction tests are rewritten against Commit (the two that proved ambient hydration rules are deleted); the pre-Cut 2 refusal test expects the attach to throw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
14d7c64774
commit
a3cc394241
10 changed files with 767 additions and 999 deletions
|
|
@ -8,7 +8,7 @@ namespace GameCult.Caching.MessagePack
|
|||
{
|
||||
public CultDocumentRegistry? Registry { get; set; }
|
||||
|
||||
public bool PullOnOpen { get; set; } = true;
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
public bool FlushOnDispose { get; set; }
|
||||
|
||||
|
|
@ -19,57 +19,32 @@ namespace GameCult.Caching.MessagePack
|
|||
|
||||
public static class CultCacheMessagePack
|
||||
{
|
||||
// Opening hydrates. A consumer that wants a fresh store removes the records it does not keep, upserts, and
|
||||
// flushes; the atomic replace does the rest.
|
||||
public static CultCache Create(string filePath, CultCacheOpenOptions? options = null)
|
||||
{
|
||||
options ??= new CultCacheOpenOptions();
|
||||
return CreateCore(filePath, options, initializeGlobals: true);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
var cache = new CultCache(options.Registry, initializeGlobals)
|
||||
options ??= new CultCacheOpenOptions();
|
||||
var cache = new CultCache(options.Registry)
|
||||
{
|
||||
FlushAttachedStoresOnDispose = options.FlushOnDispose
|
||||
};
|
||||
|
||||
if (options.UseDirectoryStore || Directory.Exists(DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath)))
|
||||
{
|
||||
cache.AddBackingStore(new DirectoryMessagePackBackingStore(filePath)
|
||||
{
|
||||
FlushOnDispose = options.StoreFlushOnDispose
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
cache.AddBackingStore(new SingleFileMessagePackBackingStore(filePath)
|
||||
{
|
||||
FlushOnDispose = options.StoreFlushOnDispose
|
||||
});
|
||||
}
|
||||
|
||||
CacheBackingStore store = options.UseDirectoryStore || Directory.Exists(DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath))
|
||||
? new DirectoryMessagePackBackingStore(filePath, readOnly: options.ReadOnly)
|
||||
: new SingleFileMessagePackBackingStore(filePath, options.ReadOnly);
|
||||
store.FlushOnDispose = options.StoreFlushOnDispose;
|
||||
cache.AddBackingStore(store);
|
||||
return cache;
|
||||
}
|
||||
|
||||
public static Task<CultCache> OpenAsync(string filePath, CultCacheOpenOptions? options = null)
|
||||
{
|
||||
return Task.FromResult(Create(filePath, options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ public static class CultDocumentMessagePackSerialization
|
|||
|
||||
public class SingleFileMessagePackBackingStore : SingleFileBackingStore
|
||||
{
|
||||
public SingleFileMessagePackBackingStore(string filePath) : base(filePath)
|
||||
public SingleFileMessagePackBackingStore(string filePath, bool readOnly = false) : base(filePath, readOnly)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
private Dictionary<string, CultPersistedRecord> _durableIndex = new(StringComparer.Ordinal);
|
||||
private CultSchemaCatalogEntry[] _durableCatalog = Array.Empty<CultSchemaCatalogEntry>();
|
||||
|
||||
public DirectoryMessagePackBackingStore(string manifestPath, string? recordDirectoryPath = null)
|
||||
public DirectoryMessagePackBackingStore(string manifestPath, string? recordDirectoryPath = null, bool readOnly = false)
|
||||
: base(readOnly)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(manifestPath))
|
||||
{
|
||||
|
|
@ -37,6 +38,8 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
|
||||
public static string DefaultRecordDirectoryPath(string manifestPath) => manifestPath + ".records";
|
||||
|
||||
public override string ToString() => _manifestFile.FullName;
|
||||
|
||||
public override void PullAll()
|
||||
{
|
||||
lock (_mutationGate)
|
||||
|
|
@ -59,7 +62,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
|
||||
var reports = new List<CultSchemaMigrationReport>();
|
||||
var loaded = new Dictionary<string, CultStoredDocument>(StringComparer.Ordinal);
|
||||
using (AcquireCommitLease())
|
||||
using (AcquireCommitLease(wait: true))
|
||||
{
|
||||
// Only pages named by the manifest read under the lease are loaded; orphaned pages are never loaded.
|
||||
var manifest = ReadManifest();
|
||||
|
|
@ -78,32 +81,26 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
Trace($"indexed-pages loaded={loaded.Count}");
|
||||
}
|
||||
|
||||
foreach (var pair in loaded)
|
||||
{
|
||||
if (_dirtyKeys.ContainsKey(pair.Key) || _deletedKeys.ContainsKey(pair.Key))
|
||||
continue;
|
||||
|
||||
if (Entries.TryGetValue(pair.Key, out var existing) &&
|
||||
string.Equals(existing.StoredAt, pair.Value.StoredAt, StringComparison.Ordinal) &&
|
||||
string.Equals(existing.Descriptor.SchemaId, pair.Value.Descriptor.SchemaId, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Entries[pair.Key] = pair.Value;
|
||||
Loaded?.Invoke(pair.Value);
|
||||
}
|
||||
|
||||
foreach (var removedKey in _hydratedKeys.Where(key => !loaded.ContainsKey(key)).ToArray())
|
||||
{
|
||||
if (_dirtyKeys.ContainsKey(removedKey) || _deletedKeys.ContainsKey(removedKey))
|
||||
continue;
|
||||
|
||||
if (Entries.TryRemove(removedKey, out var removed))
|
||||
Unloaded?.Invoke(removed);
|
||||
_hydratedKeys.Remove(removedKey);
|
||||
}
|
||||
bool Staged(string key) => _dirtyKeys.ContainsKey(key) || _deletedKeys.ContainsKey(key);
|
||||
var arrived = loaded
|
||||
.Where(pair => !Staged(pair.Key) &&
|
||||
(!Entries.TryGetValue(pair.Key, out var existing) ||
|
||||
existing.StoredAt != pair.Value.StoredAt ||
|
||||
existing.Descriptor.SchemaId != pair.Value.Descriptor.SchemaId))
|
||||
.Select(pair => pair.Value)
|
||||
.ToArray();
|
||||
var departed = _hydratedKeys
|
||||
.Where(key => !loaded.ContainsKey(key) && !Staged(key) && Entries.ContainsKey(key))
|
||||
.Select(key => Entries[key])
|
||||
.ToArray();
|
||||
if (arrived.Length > 0 || departed.Length > 0)
|
||||
Loaded?.Invoke(arrived, departed);
|
||||
|
||||
foreach (var stored in departed)
|
||||
Entries.TryRemove(stored.Key.Value, out _);
|
||||
_hydratedKeys.RemoveWhere(key => !loaded.ContainsKey(key) && !Staged(key));
|
||||
foreach (var stored in arrived)
|
||||
Entries[stored.Key.Value] = stored;
|
||||
foreach (var key in loaded.Keys)
|
||||
_hydratedKeys.Add(key);
|
||||
|
||||
|
|
@ -112,14 +109,9 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
Trace("publish");
|
||||
}
|
||||
|
||||
public override bool ContainsDurableRecord(CultRecordKey key)
|
||||
{
|
||||
lock (_mutationGate)
|
||||
return _durableIndex.ContainsKey(key.Value) || base.ContainsDurableRecord(key);
|
||||
}
|
||||
|
||||
public override void Push(CultStoredDocument entry)
|
||||
{
|
||||
ThrowIfReadOnly();
|
||||
lock (_mutationGate)
|
||||
{
|
||||
Entries[entry.Key.Value] = entry;
|
||||
|
|
@ -131,6 +123,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
|
||||
public override void Delete(CultStoredDocument entry)
|
||||
{
|
||||
ThrowIfReadOnly();
|
||||
lock (_mutationGate)
|
||||
{
|
||||
Entries.TryRemove(entry.Key.Value, out _);
|
||||
|
|
@ -140,32 +133,40 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
}
|
||||
}
|
||||
|
||||
public override void CommitBatch(
|
||||
IReadOnlyCollection<CultStoredDocument> upserts,
|
||||
IReadOnlyCollection<CultStoredDocument> deletes)
|
||||
public override CultCommitOutcome CommitBatch(CultCommitRequest request, bool wait)
|
||||
{
|
||||
ThrowIfReadOnly();
|
||||
lock (_mutationGate)
|
||||
{
|
||||
Directory.CreateDirectory(_manifestFile.DirectoryName!);
|
||||
using var commitLease = AcquireCommitLease(wait);
|
||||
if (commitLease == null)
|
||||
return CultCommitOutcome.Contended;
|
||||
var manifest = ReadManifest();
|
||||
if (!request.ConditionsHold(manifest.Records, Entries.Values))
|
||||
return CultCommitOutcome.Mismatch;
|
||||
|
||||
var previousEntries = Entries.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||
var previousDirtyKeys = _dirtyKeys.Keys.ToArray();
|
||||
var previousDeletedKeys = _deletedKeys.Keys.ToArray();
|
||||
var wasDirty = IsDirty;
|
||||
try
|
||||
{
|
||||
foreach (var entry in deletes)
|
||||
foreach (var entry in request.Deletes)
|
||||
{
|
||||
Entries.TryRemove(entry.Key.Value, out _);
|
||||
_dirtyKeys.TryRemove(entry.Key.Value, out _);
|
||||
_deletedKeys[entry.Key.Value] = true;
|
||||
}
|
||||
foreach (var entry in upserts)
|
||||
foreach (var entry in request.Upserts)
|
||||
{
|
||||
Entries[entry.Key.Value] = entry;
|
||||
_dirtyKeys[entry.Key.Value] = true;
|
||||
_deletedKeys.TryRemove(entry.Key.Value, out _);
|
||||
}
|
||||
IsDirty = true;
|
||||
PushAllCore();
|
||||
WriteGeneration(manifest);
|
||||
return CultCommitOutcome.Committed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -186,22 +187,22 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
|
||||
public override void PushAll()
|
||||
{
|
||||
ThrowIfReadOnly();
|
||||
lock (_mutationGate)
|
||||
PushAllCore();
|
||||
{
|
||||
if (!IsDirty)
|
||||
return;
|
||||
|
||||
Directory.CreateDirectory(_manifestFile.DirectoryName!);
|
||||
using var commitLease = AcquireCommitLease(wait: true);
|
||||
WriteGeneration(ReadManifest());
|
||||
}
|
||||
}
|
||||
|
||||
private void PushAllCore()
|
||||
// Runs under the commit lease: pages first, then the manifest that names them.
|
||||
private void WriteGeneration(CultPersistedStoreSnapshot currentManifest)
|
||||
{
|
||||
if (!IsDirty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_manifestFile.DirectoryName!);
|
||||
Directory.CreateDirectory(_recordDirectory.FullName);
|
||||
using var commitLease = AcquireCommitLease();
|
||||
|
||||
var currentManifest = ReadManifest();
|
||||
var currentIndex = currentManifest.Records.ToDictionary(record => record.Key, record => record, StringComparer.Ordinal);
|
||||
foreach (var key in _deletedKeys.Keys)
|
||||
currentIndex.Remove(key);
|
||||
|
|
@ -292,11 +293,10 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
var started = tracePages ? Stopwatch.GetTimestamp() : 0L;
|
||||
var metadata = records[index];
|
||||
var record = ReadPersistedRecordPage(metadata, out var pagePayload);
|
||||
var catalog = ResolveLegacyUncataloguedRecordCatalog(record, catalogEntries);
|
||||
recordReports[index] = Registry.ResolvePersistedSchemaReport(record.SchemaId, catalog);
|
||||
recordReports[index] = Registry.ResolvePersistedSchemaReport(record.SchemaId, catalogEntries);
|
||||
storedRecords[index] = ToStoredDocument(
|
||||
record,
|
||||
catalog,
|
||||
catalogEntries,
|
||||
(type, payload) => CultDocumentMessagePackSerialization.DeserializeUntyped(type, payload, Registry));
|
||||
if (tracePages)
|
||||
{
|
||||
|
|
@ -381,80 +381,6 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
return snapshot;
|
||||
}
|
||||
|
||||
// Explicit compatibility path for records written before catalog precommit existed.
|
||||
private IReadOnlyCollection<CultSchemaCatalogEntry> ResolveLegacyUncataloguedRecordCatalog(
|
||||
CultPersistedRecord record,
|
||||
IReadOnlyCollection<CultSchemaCatalogEntry> manifestCatalog)
|
||||
{
|
||||
if (manifestCatalog.Any(entry => string.Equals(entry.SchemaId, record.SchemaId, StringComparison.Ordinal)))
|
||||
{
|
||||
return manifestCatalog;
|
||||
}
|
||||
|
||||
var schemaVersion = TryReadPayloadSchemaVersion(record.Payload);
|
||||
if (string.IsNullOrWhiteSpace(schemaVersion))
|
||||
{
|
||||
return manifestCatalog;
|
||||
}
|
||||
|
||||
var schemaName = InferSchemaName(schemaVersion);
|
||||
if (string.IsNullOrWhiteSpace(schemaName))
|
||||
{
|
||||
return manifestCatalog;
|
||||
}
|
||||
|
||||
return manifestCatalog
|
||||
.Append(new CultSchemaCatalogEntry
|
||||
{
|
||||
SchemaId = record.SchemaId,
|
||||
SchemaName = schemaName,
|
||||
SchemaVersion = schemaVersion,
|
||||
ContentHash = record.SchemaId,
|
||||
CanonicalSchemaJson = "",
|
||||
CompatibleSchemaIds = Array.Empty<string>(),
|
||||
Members = Array.Empty<CultSchemaMemberCatalogEntry>()
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string TryReadPayloadSchemaVersion(byte[] payload)
|
||||
{
|
||||
if (payload == null || payload.Length == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reader = new MessagePackReader(payload);
|
||||
if (reader.NextMessagePackType != MessagePackType.Array)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (reader.ReadArrayHeader() <= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return reader.NextMessagePackType == MessagePackType.String
|
||||
? reader.ReadString() ?? ""
|
||||
: "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static string InferSchemaName(string schemaVersion)
|
||||
{
|
||||
var versionMarker = schemaVersion.LastIndexOf(".v", StringComparison.Ordinal);
|
||||
return versionMarker > 0 && versionMarker + 2 < schemaVersion.Length && char.IsDigit(schemaVersion[versionMarker + 2])
|
||||
? schemaVersion.Substring(0, versionMarker)
|
||||
: "";
|
||||
}
|
||||
|
||||
private string ContentAddressedRecordPath(CultPersistedRecord metadata)
|
||||
{
|
||||
if (metadata.Payload == null || metadata.Payload.Length != 32)
|
||||
|
|
@ -553,7 +479,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
private FileStream AcquireCommitLease()
|
||||
private FileStream? AcquireCommitLease(bool wait)
|
||||
{
|
||||
Directory.CreateDirectory(_recordDirectory.FullName);
|
||||
var lockPath = Path.Combine(_recordDirectory.FullName, ".commit.lock");
|
||||
|
|
@ -570,6 +496,10 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
bufferSize: 1,
|
||||
FileOptions.WriteThrough);
|
||||
}
|
||||
catch (IOException) when (!wait)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (IOException) when (started.Elapsed < TimeSpan.FromSeconds(30))
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -805,7 +805,7 @@ namespace GameCult.Networking
|
|||
ThrowIfDisposed();
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
|
||||
var descriptor = _cache.Registry.GetRequired<T>();
|
||||
var descriptor = _cache.Registry.GetRequired(document.GetType());
|
||||
var shard = ResolveShardInternal(descriptor, key);
|
||||
EnsurePrimary(shard, descriptor.SchemaId, key);
|
||||
|
||||
|
|
@ -840,7 +840,7 @@ namespace GameCult.Networking
|
|||
ThrowIfDisposed();
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
|
||||
var descriptor = _cache.Registry.GetRequired<T>();
|
||||
var descriptor = _cache.Registry.GetRequired(document.GetType());
|
||||
var shard = ResolveShardInternal(descriptor, key);
|
||||
EnsureClientAuthority(descriptor, key);
|
||||
|
||||
|
|
|
|||
|
|
@ -248,18 +248,12 @@ namespace GameCult.Networking
|
|||
});
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(CultNetDatabaseChangeRawMessage message)
|
||||
private Task DeleteAsync(CultNetDatabaseChangeRawMessage message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message.RecordKey) || string.IsNullOrWhiteSpace(message.SchemaId)) return;
|
||||
var descriptor = _documents.ResolveDescriptorForSchemaId(message.SchemaId!);
|
||||
var handle = Activator.CreateInstance(
|
||||
typeof(CultRecordHandle<>).MakeGenericType(descriptor.DocumentType),
|
||||
new object[] { new CultRecordKey(message.RecordKey!) });
|
||||
var task = (Task)typeof(CultCache)
|
||||
.GetMethod(nameof(CultCache.DeleteAsync), BindingFlags.Public | BindingFlags.Instance)!
|
||||
.MakeGenericMethod(descriptor.DocumentType)
|
||||
.Invoke(_cache, new[] { handle })!;
|
||||
await task.ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(message.RecordKey) || string.IsNullOrWhiteSpace(message.SchemaId)) return Task.CompletedTask;
|
||||
_documents.ResolveDescriptorForSchemaId(message.SchemaId!);
|
||||
_cache.Remove(new CultRecordKey(message.RecordKey!));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Enqueue(Func<Task> operation)
|
||||
|
|
|
|||
|
|
@ -517,7 +517,7 @@ namespace GameCult.Unity.Caching.Editor
|
|||
if (!string.IsNullOrEmpty(selected))
|
||||
{
|
||||
_path = selected;
|
||||
OpenPath(true);
|
||||
OpenPath();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -528,16 +528,16 @@ namespace GameCult.Unity.Caching.Editor
|
|||
if (!string.IsNullOrEmpty(selected))
|
||||
{
|
||||
_path = selected;
|
||||
OpenPath(false);
|
||||
OpenPath();
|
||||
SaveCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenPath(bool pullOnOpen)
|
||||
private void OpenPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
_cache = CultCacheBridge.Open(_path, pullOnOpen);
|
||||
_cache = CultCacheBridge.Open(_path);
|
||||
_selectedType = CultCacheBridge.GetDescriptors(_cache).FirstOrDefault()?.DocumentType;
|
||||
_selectedKey = string.Empty;
|
||||
EditorPrefs.SetString(LastPathKey, _path);
|
||||
|
|
@ -680,11 +680,10 @@ namespace GameCult.Unity.Caching.Editor
|
|||
|
||||
public static bool IsAvailable => CacheType != null && OpenOptionsType != null && MessagePackType != null && RecordKeyType != null;
|
||||
|
||||
public static object Open(string path, bool pullOnOpen)
|
||||
public static object Open(string path)
|
||||
{
|
||||
EnsureAvailable();
|
||||
var options = Activator.CreateInstance(OpenOptionsType);
|
||||
OpenOptionsType.GetProperty("PullOnOpen").SetValue(options, pullOnOpen, null);
|
||||
var method = MessagePackType.GetMethod("OpenAsync", new[] { typeof(string), OpenOptionsType });
|
||||
var task = (Task)method.Invoke(null, new[] { path, options });
|
||||
task.GetAwaiter().GetResult();
|
||||
|
|
|
|||
|
|
@ -285,14 +285,11 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCacheMessagePack_OpenAsync_HydratesPersistedGlobalBeforeDefault()
|
||||
public async Task CultCacheMessagePack_OpenAsync_HydratesPersistedGlobal()
|
||||
{
|
||||
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
|
||||
{
|
||||
|
|
@ -300,10 +297,8 @@ namespace GameCult.Caching.Tests
|
|||
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);
|
||||
Assert.That(seed.GetGlobal<GlobalTestEntry>(), Is.Null, "opening a store invented a global");
|
||||
await seed.UpsertAsync(new GlobalTestEntry { Value = "durable authority" });
|
||||
await seed.FlushAsync();
|
||||
}
|
||||
|
||||
|
|
@ -312,8 +307,7 @@ namespace GameCult.Caching.Tests
|
|||
new CultCacheOpenOptions { Registry = registry, UseDirectoryStore = true });
|
||||
|
||||
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");
|
||||
Assert.That(reopened.IsDirty, Is.False, "hydrating a global staged a write");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -446,15 +440,16 @@ namespace GameCult.Caching.Tests
|
|||
var recordsPath = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath);
|
||||
try
|
||||
{
|
||||
var readerStore = new DirectoryMessagePackBackingStore(filePath, recordsPath);
|
||||
var readerCache = new CultCache();
|
||||
readerCache.AddBackingStore(readerStore);
|
||||
|
||||
var writerStore = new DirectoryMessagePackBackingStore(filePath, recordsPath);
|
||||
var writerCache = new CultCache();
|
||||
writerCache.AddBackingStore(writerStore);
|
||||
await writerCache.UpsertAsync(new NamedTestEntry { Name = "source", Value = "one" });
|
||||
writerStore.PushAll();
|
||||
|
||||
var readerStore = new DirectoryMessagePackBackingStore(filePath, recordsPath);
|
||||
var readerCache = new CultCache();
|
||||
readerCache.AddBackingStore(readerStore);
|
||||
var observerRan = false;
|
||||
using var subscription = readerCache.Watch<NamedTestEntry>().Subscribe(_ =>
|
||||
{
|
||||
|
|
@ -885,49 +880,40 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_Transaction_Hides_Staged_Records_Until_Whole_Batch_Is_Durable()
|
||||
public async Task CultCache_Commit_Hides_Batch_Until_The_Directory_Store_Has_Committed_It()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-transaction-{Guid.NewGuid():N}");
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-commit-{Guid.NewGuid():N}");
|
||||
var manifest = Path.Combine(root, "state.cc");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
using var cache = new CultCache();
|
||||
cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
var firstKey = new CultRecordKey("transaction:first");
|
||||
var secondKey = new CultRecordKey("transaction:second");
|
||||
var staged = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var release = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var firstKey = new CultRecordKey("commit:first");
|
||||
var secondKey = new CultRecordKey("commit:second");
|
||||
var observed = 0;
|
||||
using var subscription = cache.Watch<NamedTestEntry>().Subscribe(_ => observed++);
|
||||
|
||||
var transaction = cache.ExecuteTransactionAsync(async () =>
|
||||
var committed = cache.Commit(batch =>
|
||||
{
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "first", Value = "one" },
|
||||
new CultRecordHandle<NamedTestEntry>(firstKey));
|
||||
staged.SetResult(true);
|
||||
await release.Task;
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "second", Value = "two" },
|
||||
new CultRecordHandle<NamedTestEntry>(secondKey));
|
||||
batch.Upsert(new NamedTestEntry { Name = "first", Value = "one" }, new CultRecordHandle<NamedTestEntry>(firstKey));
|
||||
Assert.That(cache.Get<NamedTestEntry>(firstKey), Is.Null);
|
||||
Assert.That(File.Exists(manifest), Is.False);
|
||||
batch.Upsert(new NamedTestEntry { Name = "second", Value = "two" }, new CultRecordHandle<NamedTestEntry>(secondKey));
|
||||
Assert.That(observed, Is.Zero);
|
||||
});
|
||||
|
||||
await staged.Task;
|
||||
Assert.That(cache.Get<NamedTestEntry>(firstKey), Is.Null);
|
||||
Assert.That(observed, Is.Zero);
|
||||
|
||||
release.SetResult(true);
|
||||
await transaction;
|
||||
Assert.That(committed, Is.True);
|
||||
Assert.That(cache.Get<NamedTestEntry>(firstKey)?.Value, Is.EqualTo("one"));
|
||||
Assert.That(cache.Get<NamedTestEntry>(secondKey)?.Value, Is.EqualTo("two"));
|
||||
Assert.That(observed, Is.EqualTo(2));
|
||||
Assert.That(cache.IsDirty, Is.False);
|
||||
|
||||
using var reopened = new CultCache();
|
||||
reopened.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
await reopened.PullAllBackingStoresAsync();
|
||||
Assert.That(reopened.Get<NamedTestEntry>(firstKey)?.Value, Is.EqualTo("one"));
|
||||
Assert.That(reopened.Get<NamedTestEntry>(secondKey)?.Value, Is.EqualTo("two"));
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -936,7 +922,7 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_Transaction_Abort_Cannot_Leak_Into_Later_Commit()
|
||||
public void CultCache_Commit_Stage_Failure_Cannot_Leak_Into_Later_Commit()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-abort-{Guid.NewGuid():N}");
|
||||
var manifest = Path.Combine(root, "state.cc");
|
||||
|
|
@ -945,33 +931,25 @@ namespace GameCult.Caching.Tests
|
|||
{
|
||||
using var cache = new CultCache();
|
||||
cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
var abortedKey = new CultRecordKey("transaction:aborted");
|
||||
var committedKey = new CultRecordKey("transaction:committed");
|
||||
var abortedKey = new CultRecordKey("commit:aborted");
|
||||
var committedKey = new CultRecordKey("commit:committed");
|
||||
var observed = 0;
|
||||
using var subscription = cache.Watch<NamedTestEntry>().Subscribe(_ => observed++);
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await cache.ExecuteTransactionAsync(async () =>
|
||||
{
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "aborted", Value = "must-not-leak" },
|
||||
new CultRecordHandle<NamedTestEntry>(abortedKey));
|
||||
throw new InvalidOperationException("abort probe");
|
||||
}));
|
||||
Assert.Throws<InvalidOperationException>(() => cache.Commit(batch =>
|
||||
{
|
||||
batch.Upsert(new NamedTestEntry { Name = "aborted", Value = "must-not-leak" }, new CultRecordHandle<NamedTestEntry>(abortedKey));
|
||||
throw new InvalidOperationException("abort probe");
|
||||
}));
|
||||
|
||||
Assert.That(cache.Get<NamedTestEntry>(abortedKey), Is.Null);
|
||||
Assert.That(observed, Is.Zero);
|
||||
await cache.ExecuteTransactionAsync(async () =>
|
||||
{
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "committed", Value = "survives" },
|
||||
new CultRecordHandle<NamedTestEntry>(committedKey));
|
||||
});
|
||||
Assert.That(cache.Commit(batch =>
|
||||
batch.Upsert(new NamedTestEntry { Name = "committed", Value = "survives" }, new CultRecordHandle<NamedTestEntry>(committedKey))), Is.True);
|
||||
Assert.That(observed, Is.EqualTo(1));
|
||||
|
||||
using var reopened = new CultCache();
|
||||
reopened.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
await reopened.PullAllBackingStoresAsync();
|
||||
Assert.That(reopened.Get<NamedTestEntry>(abortedKey), Is.Null);
|
||||
Assert.That(reopened.Get<NamedTestEntry>(committedKey)?.Value, Is.EqualTo("survives"));
|
||||
}
|
||||
|
|
@ -982,45 +960,33 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_Transaction_Rejects_Escaped_Async_Writes_After_Commit()
|
||||
public void CultCache_Commit_Batch_Used_After_Commit_Throws()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-escaped-transaction-{Guid.NewGuid():N}");
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-escaped-batch-{Guid.NewGuid():N}");
|
||||
var manifest = Path.Combine(root, "state.cc");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
using var cache = new CultCache();
|
||||
cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
var committedKey = new CultRecordKey("transaction:committed-parent");
|
||||
var escapedKey = new CultRecordKey("transaction:escaped-child");
|
||||
var releaseChild = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Task? escapedWrite = null;
|
||||
var committedKey = new CultRecordKey("commit:committed-parent");
|
||||
var escapedKey = new CultRecordKey("commit:escaped-child");
|
||||
CultCacheBatch? escaped = null;
|
||||
|
||||
await cache.ExecuteTransactionAsync(async () =>
|
||||
cache.Commit(batch =>
|
||||
{
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "parent", Value = "committed" },
|
||||
new CultRecordHandle<NamedTestEntry>(committedKey));
|
||||
escapedWrite = Task.Run(async () =>
|
||||
{
|
||||
await releaseChild.Task;
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "child", Value = "must-fail" },
|
||||
new CultRecordHandle<NamedTestEntry>(escapedKey));
|
||||
});
|
||||
batch.Upsert(new NamedTestEntry { Name = "parent", Value = "committed" }, new CultRecordHandle<NamedTestEntry>(committedKey));
|
||||
escaped = batch;
|
||||
});
|
||||
|
||||
releaseChild.SetResult(true);
|
||||
Assert.That(escapedWrite, Is.Not.Null);
|
||||
Assert.That(
|
||||
async () => await escapedWrite!,
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("already completed"));
|
||||
() => escaped!.Upsert(new NamedTestEntry { Name = "child", Value = "must-fail" }, new CultRecordHandle<NamedTestEntry>(escapedKey)),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("already been committed"));
|
||||
Assert.That(cache.Get<NamedTestEntry>(committedKey)?.Value, Is.EqualTo("committed"));
|
||||
Assert.That(cache.Get<NamedTestEntry>(escapedKey), Is.Null);
|
||||
|
||||
using var reopened = new CultCache();
|
||||
reopened.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
await reopened.PullAllBackingStoresAsync();
|
||||
Assert.That(reopened.Get<NamedTestEntry>(committedKey)?.Value, Is.EqualTo("committed"));
|
||||
Assert.That(reopened.Get<NamedTestEntry>(escapedKey), Is.Null);
|
||||
}
|
||||
|
|
@ -1031,118 +997,32 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_Transaction_Rejects_Hydration_Inside_Its_Mutation_Scope()
|
||||
public void CultCache_Commit_Observer_Writes_In_A_New_Commit()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-transaction-hydration-{Guid.NewGuid():N}");
|
||||
var manifest = Path.Combine(root, "state.cc");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
using (var writer = new CultCache())
|
||||
{
|
||||
writer.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
await writer.UpsertAsync(
|
||||
new NamedTestEntry { Name = "external", Value = "durable" },
|
||||
new CultRecordHandle<NamedTestEntry>(new CultRecordKey("transaction:external")));
|
||||
writer.FlushAllBackingStores();
|
||||
}
|
||||
|
||||
using var reader = new CultCache();
|
||||
reader.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
var observed = 0;
|
||||
using var subscription = reader.Watch<NamedTestEntry>().Subscribe(_ => observed++);
|
||||
|
||||
Assert.That(
|
||||
async () => await reader.ExecuteTransactionAsync(
|
||||
() => reader.PullAllBackingStoresAsync()),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("hydrate before"));
|
||||
Assert.That(reader.Get<NamedTestEntry>(new CultRecordKey("transaction:external")), Is.Null);
|
||||
Assert.That(observed, Is.Zero);
|
||||
|
||||
await reader.PullAllBackingStoresAsync();
|
||||
Assert.That(reader.Get<NamedTestEntry>(new CultRecordKey("transaction:external"))?.Value,
|
||||
Is.EqualTo("durable"));
|
||||
Assert.That(observed, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_Hydration_Waits_For_The_Active_Mutation_Transaction()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-hydration-gate-{Guid.NewGuid():N}");
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-observer-commit-{Guid.NewGuid():N}");
|
||||
var manifest = Path.Combine(root, "state.cc");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
using var cache = new CultCache();
|
||||
cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
var staged = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var release = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var transaction = cache.ExecuteTransactionAsync(async () =>
|
||||
{
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "staged", Value = "committed" },
|
||||
new CultRecordHandle<NamedTestEntry>(new CultRecordKey("transaction:hydration-gate")));
|
||||
staged.SetResult(true);
|
||||
await release.Task;
|
||||
});
|
||||
|
||||
await staged.Task;
|
||||
var hydration = cache.PullAllBackingStoresAsync();
|
||||
Assert.That(hydration.IsCompleted, Is.False,
|
||||
"hydration must wait until the active mutation transaction has committed");
|
||||
|
||||
release.SetResult(true);
|
||||
await transaction;
|
||||
await hydration;
|
||||
Assert.That(cache.Get<NamedTestEntry>(new CultRecordKey("transaction:hydration-gate"))?.Value,
|
||||
Is.EqualTo("committed"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_Transaction_Observer_Writes_In_A_New_Commit()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"cultcache-observer-transaction-{Guid.NewGuid():N}");
|
||||
var manifest = Path.Combine(root, "state.cc");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
using var cache = new CultCache();
|
||||
cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
var firstKey = new CultRecordKey("transaction:observer-source");
|
||||
var secondKey = new CultRecordKey("transaction:observer-write");
|
||||
var firstKey = new CultRecordKey("commit:observer-source");
|
||||
var secondKey = new CultRecordKey("commit:observer-write");
|
||||
using var subscription = cache.Watch<NamedTestEntry>().Subscribe(change =>
|
||||
{
|
||||
if (!change.Key.Equals(firstKey)) return;
|
||||
cache.ExecuteTransactionAsync(async () =>
|
||||
{
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "observer", Value = "separate-commit" },
|
||||
new CultRecordHandle<NamedTestEntry>(secondKey));
|
||||
}).GetAwaiter().GetResult();
|
||||
cache.Commit(batch => batch.Upsert(
|
||||
new NamedTestEntry { Name = "observer", Value = "separate-commit" },
|
||||
new CultRecordHandle<NamedTestEntry>(secondKey)));
|
||||
});
|
||||
|
||||
await cache.ExecuteTransactionAsync(async () =>
|
||||
{
|
||||
await cache.UpsertAsync(
|
||||
new NamedTestEntry { Name = "source", Value = "committed-first" },
|
||||
new CultRecordHandle<NamedTestEntry>(firstKey));
|
||||
});
|
||||
cache.Commit(batch => batch.Upsert(
|
||||
new NamedTestEntry { Name = "source", Value = "committed-first" },
|
||||
new CultRecordHandle<NamedTestEntry>(firstKey)));
|
||||
|
||||
Assert.That(cache.Get<NamedTestEntry>(secondKey)?.Value, Is.EqualTo("separate-commit"));
|
||||
using var reopened = new CultCache();
|
||||
reopened.AddBackingStore(new DirectoryMessagePackBackingStore(manifest));
|
||||
await reopened.PullAllBackingStoresAsync();
|
||||
Assert.That(reopened.Get<NamedTestEntry>(firstKey)?.Value, Is.EqualTo("committed-first"));
|
||||
Assert.That(reopened.Get<NamedTestEntry>(secondKey)?.Value, Is.EqualTo("separate-commit"));
|
||||
}
|
||||
|
|
@ -1170,6 +1050,7 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
[CultDocument("tests.global_entry", "tests.global_entry.v1")]
|
||||
[CultGlobal]
|
||||
internal sealed class GlobalTestEntry
|
||||
{
|
||||
[Key(0)]
|
||||
|
|
|
|||
|
|
@ -104,11 +104,10 @@ namespace GameCult.Caching.Tests
|
|||
File.WriteAllBytes(manifest, bytes);
|
||||
|
||||
using var cache = new CultCache();
|
||||
var store = new DirectoryMessagePackBackingStore(manifest);
|
||||
cache.AddBackingStore(store);
|
||||
|
||||
Assert.That(() => store.PullAll(),
|
||||
Assert.That(() => cache.AddBackingStore(new DirectoryMessagePackBackingStore(manifest)),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains(formatVersion));
|
||||
Assert.That(cache.BackingStores, Is.Empty);
|
||||
Assert.That(File.ReadAllBytes(manifest), Is.EqualTo(bytes));
|
||||
}
|
||||
finally
|
||||
|
|
|
|||
|
|
@ -1761,8 +1761,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = sourceCache.Registry,
|
||||
PullOnOpen = false
|
||||
Registry = sourceCache.Registry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
@ -1820,8 +1819,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = sourceCache.Registry,
|
||||
PullOnOpen = false
|
||||
Registry = sourceCache.Registry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
@ -1846,8 +1844,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = sourceCache.Registry,
|
||||
PullOnOpen = false
|
||||
Registry = sourceCache.Registry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
@ -1899,8 +1896,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = sourceCache.Registry,
|
||||
PullOnOpen = false
|
||||
Registry = sourceCache.Registry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
@ -2760,8 +2756,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = sourceCache.Registry,
|
||||
PullOnOpen = false
|
||||
Registry = sourceCache.Registry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
@ -2835,8 +2830,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = targetCache.Registry,
|
||||
PullOnOpen = false
|
||||
Registry = targetCache.Registry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
@ -2993,8 +2987,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = cacheRegistry,
|
||||
PullOnOpen = false
|
||||
Registry = cacheRegistry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
@ -3030,8 +3023,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = sourceCache.Registry,
|
||||
PullOnOpen = false
|
||||
Registry = sourceCache.Registry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
@ -3170,8 +3162,7 @@ public sealed class CultMeshStreamingTests
|
|||
StartServer = false,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = sourceCache.Registry,
|
||||
PullOnOpen = false
|
||||
Registry = sourceCache.Registry
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue