diff --git a/Directory.Build.props b/Directory.Build.props index 3c84b8a5..5cc68296 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,7 +14,7 @@ MIT © Meta Vens and contributors true - 1.0.17 + 1.0.18 true diff --git a/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs b/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs index 1597be2a..77528970 100644 --- a/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs +++ b/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs @@ -75,6 +75,9 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore 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)) @@ -91,14 +94,15 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore foreach (var removedKey in Entries.Keys.Where(key => !loaded.ContainsKey(key)).ToArray()) { + if (_dirtyKeys.ContainsKey(removedKey) || _deletedKeys.ContainsKey(removedKey)) + continue; + if (Entries.TryRemove(removedKey, out var removed)) EntryDeleted.OnNext(removed); } SetLastSchemaMigrationReports(reports); - _dirtyKeys.Clear(); - _deletedKeys.Clear(); - IsDirty = false; + IsDirty = !_dirtyKeys.IsEmpty || !_deletedKeys.IsEmpty; } /// diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs index ba9bdbdf..b06391be 100644 --- a/src/GameCult.Caching/CultCache.cs +++ b/src/GameCult.Caching/CultCache.cs @@ -2092,6 +2092,13 @@ namespace GameCult.Caching return; } + // A single-file snapshot cannot distinguish local dirty keys from clean keys. + // Pulling while local mutations are staged would let an older disk snapshot + // erase those mutations before the next flush. Flush first; clean readers can + // still poll external snapshots incrementally. + if (IsDirty) + return; + CultPersistedStoreSnapshot snapshot; try { diff --git a/tests/GameCult.Caching.Tests/BackingStoreTests.cs b/tests/GameCult.Caching.Tests/BackingStoreTests.cs index 328b778e..9e9a7606 100644 --- a/tests/GameCult.Caching.Tests/BackingStoreTests.cs +++ b/tests/GameCult.Caching.Tests/BackingStoreTests.cs @@ -107,6 +107,53 @@ namespace GameCult.Caching.Tests } } + [Test] + public async Task SingleFileMessagePackBackingStore_PullAll_DoesNotEraseUnflushedLocalMutations() + { + var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.msgpack"); + + try + { + using (var seed = await CultCacheMessagePack.OpenAsync(filePath)) + { + await seed.UpsertAsync(new NamedTestEntry + { + Name = "persisted", + Value = "before-local-write" + }); + await seed.FlushAsync(); + } + + CultRecordHandle localHandle; + using (var cache = await CultCacheMessagePack.OpenAsync(filePath)) + { + localHandle = await cache.UpsertAsync(new NamedTestEntry + { + Name = "local", + Value = "must-survive-pull" + }); + + Assert.That(cache.IsDirty, Is.True); + await cache.PullAllBackingStoresAsync(); + + Assert.That(cache.Get(localHandle.Key)?.Value, + Is.EqualTo("must-survive-pull")); + Assert.That(cache.IsDirty, Is.True, + "pulling a clean disk snapshot must not pardon staged local mutations"); + await cache.FlushAsync(); + } + + using var reopened = await CultCacheMessagePack.OpenAsync(filePath); + Assert.That(reopened.Get(localHandle.Key)?.Value, + Is.EqualTo("must-survive-pull")); + } + finally + { + if (File.Exists(filePath)) + File.Delete(filePath); + } + } + [Test] public async Task CultCache_DirtyState_Tracks_Mutations_And_ExplicitFlush() { @@ -425,6 +472,60 @@ namespace GameCult.Caching.Tests } } + [Test] + public async Task DirectoryMessagePackBackingStore_PullAll_PreservesUnflushedLocalKeys() + { + var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.cc"); + var recordsPath = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath); + + try + { + using (var seed = await CultCacheMessagePack.OpenAsync( + filePath, + new CultCacheOpenOptions { UseDirectoryStore = true })) + { + await seed.UpsertAsync(new NamedTestEntry + { + Name = "persisted", + Value = "before-local-write" + }); + await seed.FlushAsync(); + } + + CultRecordHandle localHandle; + using (var cache = await CultCacheMessagePack.OpenAsync( + filePath, + new CultCacheOpenOptions { UseDirectoryStore = true })) + { + localHandle = await cache.UpsertAsync(new NamedTestEntry + { + Name = "local", + Value = "must-survive-pull" + }); + + await cache.PullAllBackingStoresAsync(); + + Assert.That(cache.Get(localHandle.Key)?.Value, + Is.EqualTo("must-survive-pull")); + Assert.That(cache.IsDirty, Is.True); + await cache.FlushAsync(); + } + + using var reopened = await CultCacheMessagePack.OpenAsync( + filePath, + new CultCacheOpenOptions { UseDirectoryStore = true }); + Assert.That(reopened.Get(localHandle.Key)?.Value, + Is.EqualTo("must-survive-pull")); + } + finally + { + if (File.Exists(filePath)) + File.Delete(filePath); + if (Directory.Exists(recordsPath)) + Directory.Delete(recordsPath, recursive: true); + } + } + [Test] public async Task DirectoryMessagePackBackingStore_Remains_Dirty_When_Deletion_Fails() { diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll index a04121ee..8786becc 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb index 52e90dd3..20d98383 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll index 13dda8a4..558b6852 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb index e5fa0f25..2c5a3cae 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll index bbb19751..aa345f22 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb index 48df96f5..a4db4304 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll index 4c209fbe..551c17e8 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb index 23a5cc92..88694cb6 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll index 9d8d79cb..551dbfcb 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb index 4175bf71..41c757da 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb differ diff --git a/unity/org.gamecult.cultlib/package.json b/unity/org.gamecult.cultlib/package.json index 66fcfa90..c235eb67 100644 --- a/unity/org.gamecult.cultlib/package.json +++ b/unity/org.gamecult.cultlib/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.cultlib", "displayName": "CultLib", - "version": "1.0.17", + "version": "1.0.18", "unity": "2021.3", "description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.", "license": "MIT",