fix(site-runtime): site SQLite runs WAL with an explicit busy-timeout floor (S8) — concurrent writers stop serializing on the rollback journal
This commit is contained in:
@@ -20,15 +20,32 @@ public class SiteStorageService
|
||||
/// <param name="logger">Logger instance for diagnostic messages.</param>
|
||||
public SiteStorageService(string connectionString, ILogger<SiteStorageService> logger)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
// Normalize the connection string and pin a busy-timeout floor (S8). WAL lets a reader
|
||||
// and a writer proceed concurrently, but two writers still contend; Microsoft.Data.Sqlite
|
||||
// drives its busy handler off the command timeout, so a floor of 5 s means a briefly
|
||||
// busy database is waited-on rather than failing fast with SQLITE_BUSY. Only raise a
|
||||
// caller-supplied value that is lower than the floor (the library default of 30 s stays).
|
||||
var builder = new SqliteConnectionStringBuilder(connectionString);
|
||||
if (builder.DefaultTimeout < BusyTimeoutFloorSeconds)
|
||||
builder.DefaultTimeout = BusyTimeoutFloorSeconds;
|
||||
_connectionString = builder.ToString();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>Busy-timeout floor in seconds applied to the site connection string (S8).</summary>
|
||||
private const int BusyTimeoutFloorSeconds = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new (unopened) SQLite connection against the site database.
|
||||
/// Exposed so site-local repositories can open their own connections without
|
||||
/// reaching into private state via reflection. The caller owns
|
||||
/// the connection and is responsible for opening and disposing it.
|
||||
/// <para>
|
||||
/// The database runs in WAL journal mode (set once in <see cref="InitializeAsync"/>) with a
|
||||
/// busy-timeout floor (see the constructor), so connections handed out here inherit
|
||||
/// concurrent-reader/writer behavior and wait out a briefly-busy database instead of
|
||||
/// failing with SQLITE_BUSY.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A new, unopened <see cref="SqliteConnection"/> against the site database.</returns>
|
||||
public SqliteConnection CreateConnection() => new(_connectionString);
|
||||
@@ -43,6 +60,21 @@ public class SiteStorageService
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
// Switch to WAL journal mode (S8). WAL is persistent (database-level, survives across
|
||||
// connections) so this one-time set is enough. It lets readers and a writer proceed
|
||||
// concurrently instead of every access serializing on the rollback journal. A
|
||||
// :memory: database or a filesystem that cannot support WAL falls back to its prior
|
||||
// mode — we log the result rather than throwing.
|
||||
await using (var walCommand = connection.CreateCommand())
|
||||
{
|
||||
walCommand.CommandText = "PRAGMA journal_mode=WAL;";
|
||||
var resultingMode = (await walCommand.ExecuteScalarAsync()) as string ?? "unknown";
|
||||
if (!string.Equals(resultingMode, "wal", StringComparison.OrdinalIgnoreCase))
|
||||
_logger.LogWarning(
|
||||
"Site SQLite could not enable WAL journal mode (got '{Mode}') — concurrent access may serialize",
|
||||
resultingMode);
|
||||
}
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS deployed_configurations (
|
||||
|
||||
@@ -42,6 +42,19 @@ public class SiteStorageServiceTests : IAsyncLifetime, IDisposable
|
||||
await _storage.InitializeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Initialize_EnablesWalJournalMode()
|
||||
{
|
||||
// WAL is set once at InitializeAsync (persistent, database-level). A file-backed DB
|
||||
// is required — WAL is not available for :memory: databases.
|
||||
await using var conn = _storage.CreateConnection();
|
||||
await conn.OpenAsync();
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA journal_mode;";
|
||||
var mode = (string)(await cmd.ExecuteScalarAsync())!;
|
||||
Assert.Equal("wal", mode.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StoreAndRetrieve_DeployedConfig_RoundTrips()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user