Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/BLite.Core/Storage/LockTimeout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ namespace BLite.Core.Storage;
/// <c>busy_timeout</c> is 0) or specify custom values to give concurrent callers
/// time to release contended locks.
/// </para>
/// <para>
/// <c>default(LockTimeout)</c>, or <c>new LockTimeout()</c> with no property set, means
/// "not specified": <see cref="PageFileConfig.Normalize"/> replaces it with
/// <see cref="Default"/>. To fail fast, set <see cref="Immediate"/> explicitly.
/// </para>
/// </summary>
public readonly struct LockTimeout : IEquatable<LockTimeout>
{
Expand All @@ -25,7 +30,7 @@ namespace BLite.Core.Storage;
/// <item><c>&gt;0</c> — wait up to this many milliseconds.</item>
/// </list>
/// </summary>
public int ReadTimeoutMs { get; init; }
public int ReadTimeoutMs { get => _readTimeoutMs; init { _readTimeoutMs = value; _isSpecified = true; } }

/// <summary>
/// Timeout for acquiring write locks (milliseconds).
Expand All @@ -35,7 +40,7 @@ namespace BLite.Core.Storage;
/// <item><c>&gt;0</c> — wait up to this many milliseconds.</item>
/// </list>
/// </summary>
public int WriteTimeoutMs { get; init; }
public int WriteTimeoutMs { get => _writeTimeoutMs; init { _writeTimeoutMs = value; _isSpecified = true; } }

/// <summary>
/// Maximum number of threads that can simultaneously enter the commit path.
Expand All @@ -50,7 +55,18 @@ namespace BLite.Core.Storage;
/// <item><c>&gt;0</c> — at most this many concurrent commit operations.</item>
/// </list>
/// </summary>
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;

/// <summary>
/// <c>false</c> for <c>default(LockTimeout)</c>: no property was set, so the value is
/// indistinguishable from <see cref="Immediate"/> but was not chosen. Not part of equality.
/// </summary>
internal bool IsSpecified => _isSpecified;

/// <summary>
/// Immediate failure on contention — equivalent to SQLite's default
Expand Down
9 changes: 6 additions & 3 deletions src/BLite.Core/Storage/PageFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// <see cref="System.TimeoutException"/> (analogous to SQLite's <c>SQLITE_BUSY</c>).
/// Defaults to <see cref="LockTimeout.Default"/> (500 ms read / 500 ms write).
/// Defaults to <see cref="LockTimeout.Default"/> (500 ms read / 500 ms write), also when
/// the config is built with an object initializer that does not set it.
/// Use <see cref="LockTimeout.Immediate"/> for fail-fast behaviour.
/// </summary>
public LockTimeout LockTimeout { get; init; }
Expand Down Expand Up @@ -133,8 +134,9 @@ public readonly struct PageFileConfig
public bool AllowMultiProcessAccess { get; init; }

/// <summary>
/// Returns a copy of this config where any zero-valued numeric fields are replaced
/// with the corresponding values from <see cref="Default"/>.
/// Returns a copy of this config where any zero-valued numeric fields, and a
/// <see cref="LockTimeout"/> that was never set, are replaced with the corresponding
/// values from <see cref="Default"/>.
/// This ensures that a partially-constructed <see cref="PageFileConfig"/>
/// (e.g. <c>new PageFileConfig { AllowMultiProcessAccess = true }</c>) is always
/// valid before being handed to the storage engine.
Expand All @@ -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,
};
}

Expand Down
34 changes: 34 additions & 0 deletions tests/BLite.Tests/PageFileConfigLockTimeoutTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using BLite.Core.Storage;

namespace BLite.Tests;

/// <summary>
/// Regression test for #322: a <see cref="PageFileConfig"/> built with an object initializer
/// that does not set <see cref="PageFileConfig.LockTimeout"/> used to open with
/// <c>default(LockTimeout)</c>, which has the same values as <see cref="LockTimeout.Immediate"/>,
/// so every contended lock failed at once.
/// </summary>
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 */ }
}
}
}
Loading