diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs
index de559034..7391a057 100644
--- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs
@@ -20,15 +20,32 @@ public class SiteStorageService
/// Logger instance for diagnostic messages.
public SiteStorageService(string connectionString, ILogger 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;
}
+ /// Busy-timeout floor in seconds applied to the site connection string (S8).
+ private const int BusyTimeoutFloorSeconds = 5;
+
///
/// 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.
+ ///
+ /// The database runs in WAL journal mode (set once in ) 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.
+ ///
///
/// A new, unopened against the site database.
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 (
diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/SiteStorageServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/SiteStorageServiceTests.cs
index 8adcb42d..549a73ad 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/SiteStorageServiceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/SiteStorageServiceTests.cs
@@ -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()
{