diff --git a/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs b/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs index b15bdbb1..5a98ff08 100644 --- a/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs +++ b/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs @@ -57,7 +57,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore var reports = new List(); var loaded = new Dictionary(StringComparer.Ordinal); CultSchemaCatalogEntry[] catalog; - using (AcquireCommitLease(wait: true)) + using (AcquireCommitLease(wait: true, create: false)) { // Only pages named by the manifest read under the lease are loaded; orphaned pages are never loaded. var manifest = ReadManifest(); @@ -131,7 +131,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore return Held(() => { Directory.CreateDirectory(_manifestFile.DirectoryName!); - using var commitLease = AcquireCommitLease(wait); + using var commitLease = AcquireCommitLease(wait, create: true); if (commitLease == null) return CultCommitOutcome.Contended; var manifest = ReadManifest(); @@ -186,7 +186,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore return; Directory.CreateDirectory(_manifestFile.DirectoryName!); - using var commitLease = AcquireCommitLease(wait: true); + using var commitLease = AcquireCommitLease(wait: true, create: true); WriteGeneration(ReadManifest()); }); } @@ -470,9 +470,12 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore return buffer.ToArray(); } - private FileStream? AcquireCommitLease(bool wait) + // Only a writer creates the records folder and the lock. A reader opens an existing lock; with none, no writer has ever + // committed here, so there is nothing to exclude and opening the store creates nothing. + private FileStream? AcquireCommitLease(bool wait, bool create) { - Directory.CreateDirectory(_recordDirectory.FullName); + if (create) + Directory.CreateDirectory(_recordDirectory.FullName); var lockPath = Path.Combine(_recordDirectory.FullName, ".commit.lock"); var started = Stopwatch.StartNew(); while (true) @@ -481,12 +484,20 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore { return new FileStream( lockPath, - FileMode.OpenOrCreate, + create ? FileMode.OpenOrCreate : FileMode.Open, FileAccess.ReadWrite, FileShare.None, bufferSize: 1, FileOptions.WriteThrough); } + catch (FileNotFoundException) when (!create) + { + return null; + } + catch (DirectoryNotFoundException) when (!create) + { + return null; + } catch (IOException) when (!wait) { return null; diff --git a/src/GameCult.Unity/Assets/Caching/Runtime.meta b/src/GameCult.Unity/Assets/Caching/Runtime.meta deleted file mode 100644 index 5142c828..00000000 --- a/src/GameCult.Unity/Assets/Caching/Runtime.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 5a7d06cb8f1e491785b3ae24034f1121 -timeCreated: 1779630000 diff --git a/src/GameCult.Unity/Assets/Caching/Runtime/CultCacheInspectorAttributes.cs b/src/GameCult.Unity/Assets/Caching/Runtime/CultCacheInspectorAttributes.cs deleted file mode 100644 index 1005322c..00000000 --- a/src/GameCult.Unity/Assets/Caching/Runtime/CultCacheInspectorAttributes.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; - -namespace GameCult.Unity.Caching -{ - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class, Inherited = true)] - public sealed class CultInspectorLabelAttribute : Attribute - { - public CultInspectorLabelAttribute(string label) - { - Label = label ?? string.Empty; - } - - public string Label { get; } - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] - public sealed class CultInspectorHiddenAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] - public sealed class CultInspectorReadOnlyAttribute : Attribute - { - } - - // Lower values appear first; members without it keep their key order. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] - public sealed class CultInspectorOrderAttribute : Attribute - { - public CultInspectorOrderAttribute(int order) - { - Order = order; - } - - public int Order { get; } - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] - public sealed class CultInspectorTextAreaAttribute : Attribute - { - public CultInspectorTextAreaAttribute(int minLines = 3, int maxLines = 12) - { - MinLines = minLines; - MaxLines = maxLines; - } - - public int MinLines { get; } - public int MaxLines { get; } - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] - public sealed class CultInspectorRangeAttribute : Attribute - { - public CultInspectorRangeAttribute(float min, float max) - { - Min = min; - Max = max; - } - - public float Min { get; } - public float Max { get; } - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] - public sealed class CultInspectorAssetPathAttribute : Attribute - { - public CultInspectorAssetPathAttribute(Type assetType = null) - { - AssetType = assetType; - } - - public Type AssetType { get; } - } - - // Marks an editor class implementing ICultInspectorDrawer as the drawer for every member of MemberType, - // or of any closed form when MemberType is an open generic definition. It wins over the built-in drawers. - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class CultInspectorDrawerAttribute : Attribute - { - public CultInspectorDrawerAttribute(Type memberType) - { - MemberType = memberType ?? throw new ArgumentNullException(nameof(memberType)); - } - - public Type MemberType { get; } - } -} diff --git a/src/GameCult.Unity/Assets/Caching/Runtime/CultCacheInspectorAttributes.cs.meta b/src/GameCult.Unity/Assets/Caching/Runtime/CultCacheInspectorAttributes.cs.meta deleted file mode 100644 index ecc92c68..00000000 --- a/src/GameCult.Unity/Assets/Caching/Runtime/CultCacheInspectorAttributes.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 8871d3eb346242e3b0acfd6da5108676 -timeCreated: 1779630000 diff --git a/src/GameCult.Unity/Assets/Caching/Runtime/GameCult.Unity.Caching.asmdef b/src/GameCult.Unity/Assets/Caching/Runtime/GameCult.Unity.Caching.asmdef deleted file mode 100644 index 6e7e12a6..00000000 --- a/src/GameCult.Unity/Assets/Caching/Runtime/GameCult.Unity.Caching.asmdef +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "GameCult.Unity.Caching", - "rootNamespace": "GameCult.Unity.Caching", - "references": [], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": true -} diff --git a/src/GameCult.Unity/Assets/Caching/Runtime/GameCult.Unity.Caching.asmdef.meta b/src/GameCult.Unity/Assets/Caching/Runtime/GameCult.Unity.Caching.asmdef.meta deleted file mode 100644 index 00ae3475..00000000 --- a/src/GameCult.Unity/Assets/Caching/Runtime/GameCult.Unity.Caching.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: ab6636c1402c4c579df9c07101542f13 -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/tests/GameCult.Caching.Tests/BackingStoreTests.cs b/tests/GameCult.Caching.Tests/BackingStoreTests.cs index 98ce654e..be868959 100644 --- a/tests/GameCult.Caching.Tests/BackingStoreTests.cs +++ b/tests/GameCult.Caching.Tests/BackingStoreTests.cs @@ -284,6 +284,55 @@ namespace GameCult.Caching.Tests } } + [Test] + public async Task DirectoryMessagePackBackingStore_Open_CreatesNothing_UntilAWrite() + { + var root = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}"); + Directory.CreateDirectory(root); + var filePath = Path.Combine(root, "store.cc"); + var recordsPath = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath); + + try + { + foreach (var readOnly in new[] { true, false }) + { + using (await CultCacheMessagePack.OpenAsync(filePath, new CultCacheOpenOptions { UseDirectoryStore = true, ReadOnly = readOnly })) + { + } + + Assert.That(Directory.GetFileSystemEntries(root), Is.Empty, + $"opening a missing directory store (read-only {readOnly}) created files"); + } + + Directory.CreateDirectory(recordsPath); + foreach (var readOnly in new[] { true, false }) + { + using (var cache = await CultCacheMessagePack.OpenAsync(filePath, new CultCacheOpenOptions { UseDirectoryStore = true, ReadOnly = readOnly })) + { + Assert.That(cache.AllStoredDocuments, Is.Empty); + } + + Assert.That(Directory.GetFileSystemEntries(recordsPath), Is.Empty, + $"opening a manifest-less directory store (read-only {readOnly}) created a lock"); + Assert.That(File.Exists(filePath), Is.False); + } + + using (var writer = await CultCacheMessagePack.OpenAsync(filePath, new CultCacheOpenOptions { UseDirectoryStore = true })) + { + await writer.UpsertAsync(new NamedTestEntry { Name = "first-write", Value = "creates the store" }); + await writer.FlushAsync(); + } + + Assert.That(File.Exists(filePath), Is.True); + Assert.That(File.Exists(Path.Combine(recordsPath, ".commit.lock")), Is.True); + Assert.That(Directory.GetFiles(recordsPath, "*.msgpack"), Has.Length.EqualTo(1)); + } + finally + { + if (Directory.Exists(root)) Directory.Delete(root, recursive: true); + } + } + [Test] public async Task CultCacheMessagePack_OpenAsync_HydratesPersistedGlobal() {