diff --git a/src/BLite.Core/Storage/LockTimeout.cs b/src/BLite.Core/Storage/LockTimeout.cs index 1069399..4503f85 100644 --- a/src/BLite.Core/Storage/LockTimeout.cs +++ b/src/BLite.Core/Storage/LockTimeout.cs @@ -14,6 +14,11 @@ namespace BLite.Core.Storage; /// busy_timeout is 0) or specify custom values to give concurrent callers /// time to release contended locks. /// +/// +/// default(LockTimeout), or new LockTimeout() with no property set, means +/// "not specified": replaces it with +/// . To fail fast, set explicitly. +/// /// public readonly struct LockTimeout : IEquatable { @@ -25,7 +30,7 @@ namespace BLite.Core.Storage; /// >0 — wait up to this many milliseconds. /// /// - public int ReadTimeoutMs { get; init; } + public int ReadTimeoutMs { get => _readTimeoutMs; init { _readTimeoutMs = value; _isSpecified = true; } } /// /// Timeout for acquiring write locks (milliseconds). @@ -35,7 +40,7 @@ namespace BLite.Core.Storage; /// >0 — wait up to this many milliseconds. /// /// - public int WriteTimeoutMs { get; init; } + public int WriteTimeoutMs { get => _writeTimeoutMs; init { _writeTimeoutMs = value; _isSpecified = true; } } /// /// Maximum number of threads that can simultaneously enter the commit path. @@ -50,7 +55,18 @@ namespace BLite.Core.Storage; /// >0 — at most this many concurrent commit operations. /// /// - public int MaxConcurrentWriters { get; init; } + public int MaxConcurrentWriters { get => _maxConcurrentWriters; init { _maxConcurrentWriters = value; _isSpecified = true; } } + + private readonly int _readTimeoutMs; + private readonly int _writeTimeoutMs; + private readonly int _maxConcurrentWriters; + private readonly bool _isSpecified; + + /// + /// false for default(LockTimeout): no property was set, so the value is + /// indistinguishable from but was not chosen. Not part of equality. + /// + internal bool IsSpecified => _isSpecified; /// /// Immediate failure on contention — equivalent to SQLite's default diff --git a/src/BLite.Core/Storage/PageFile.cs b/src/BLite.Core/Storage/PageFile.cs index 2936ed2..9f29fef 100644 --- a/src/BLite.Core/Storage/PageFile.cs +++ b/src/BLite.Core/Storage/PageFile.cs @@ -46,7 +46,8 @@ public readonly struct PageFileConfig /// Lock acquisition timeouts for read and write operations. /// Controls how long the engine waits for a contended lock before throwing /// (analogous to SQLite's SQLITE_BUSY). - /// Defaults to (500 ms read / 500 ms write). + /// Defaults to (500 ms read / 500 ms write), also when + /// the config is built with an object initializer that does not set it. /// Use for fail-fast behaviour. /// public LockTimeout LockTimeout { get; init; } @@ -133,8 +134,9 @@ public readonly struct PageFileConfig public bool AllowMultiProcessAccess { get; init; } /// - /// Returns a copy of this config where any zero-valued numeric fields are replaced - /// with the corresponding values from . + /// Returns a copy of this config where any zero-valued numeric fields, and a + /// that was never set, are replaced with the corresponding + /// values from . /// This ensures that a partially-constructed /// (e.g. new PageFileConfig { AllowMultiProcessAccess = true }) is always /// valid before being handed to the storage engine. @@ -146,6 +148,7 @@ public PageFileConfig Normalize() { PageSize = PageSize > 0 ? PageSize : defaults.PageSize, GrowthBlockSize = GrowthBlockSize > 0 ? GrowthBlockSize : defaults.GrowthBlockSize, + LockTimeout = LockTimeout.IsSpecified ? LockTimeout : defaults.LockTimeout, }; } diff --git a/tests/BLite.Tests/PageFileConfigLockTimeoutTests.cs b/tests/BLite.Tests/PageFileConfigLockTimeoutTests.cs new file mode 100644 index 0000000..c25b1ff --- /dev/null +++ b/tests/BLite.Tests/PageFileConfigLockTimeoutTests.cs @@ -0,0 +1,34 @@ +using BLite.Core.Storage; + +namespace BLite.Tests; + +/// +/// Regression test for #322: a built with an object initializer +/// that does not set used to open with +/// default(LockTimeout), which has the same values as , +/// so every contended lock failed at once. +/// +public class PageFileConfigLockTimeoutTests +{ + [Fact] + public void StorageEngine_PartialConfigWithoutLockTimeout_GetsDefault_ExplicitImmediateIsKept() + { + var dir = Path.Combine(Path.GetTempPath(), $"blite_locktimeout_{Guid.NewGuid():N}"); + Directory.CreateDirectory(dir); + try + { + using (var engine = new StorageEngine(Path.Combine(dir, "partial.db"), new PageFileConfig { AllowMultiProcessAccess = false })) + Assert.Equal(LockTimeout.Default, engine.LockTimeout); + + using (var engine = new StorageEngine(Path.Combine(dir, "immediate.db"), new PageFileConfig { LockTimeout = LockTimeout.Immediate })) + Assert.Equal(LockTimeout.Immediate, engine.LockTimeout); + + using (var engine = new StorageEngine(Path.Combine(dir, "from.db"), new PageFileConfig { LockTimeout = LockTimeout.From(0, 0, 0) })) + Assert.Equal(LockTimeout.Immediate, engine.LockTimeout); + } + finally + { + try { Directory.Delete(dir, recursive: true); } catch { /* best-effort */ } + } + } +}