using Microsoft.Data.Sqlite; using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Server.Configuration; namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication; /// /// Factory for creating SQLite connections to the authentication store. /// public sealed class AuthSqliteConnectionFactory(IOptions options) { /// /// Busy timeout applied to every auth-store connection. SQLite retries a busy /// database for this long before surfacing SQLITE_BUSY, so the concurrent /// MarkKeyUsedAsync / audit-append writers degrade gracefully under load /// instead of failing the request path. /// private static readonly TimeSpan BusyTimeout = TimeSpan.FromSeconds(5); /// /// Creates an unopened SQLite connection to the auth database. Prefer /// , which also applies WAL journaling and the /// busy timeout. /// public SqliteConnection CreateConnection() { string sqlitePath = options.Value.Authentication.SqlitePath; string? directory = Path.GetDirectoryName(sqlitePath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } SqliteConnectionStringBuilder builder = new() { DataSource = sqlitePath, Mode = SqliteOpenMode.ReadWriteCreate, Pooling = true, DefaultTimeout = (int)BusyTimeout.TotalSeconds, }; return new SqliteConnection(builder.ToString()); } /// /// Creates a SQLite connection, opens it, and configures WAL journaling and a /// non-zero busy timeout so concurrent readers and writers degrade gracefully /// rather than surfacing SQLITE_BUSY as a hard failure. /// /// Cancellation token for the operation. /// An opened and configured SQLite connection. public async Task OpenConnectionAsync(CancellationToken cancellationToken) { SqliteConnection connection = CreateConnection(); try { await connection.OpenAsync(cancellationToken).ConfigureAwait(false); await ConfigureConnectionAsync(connection, cancellationToken).ConfigureAwait(false); return connection; } catch { await connection.DisposeAsync().ConfigureAwait(false); throw; } } private static async Task ConfigureConnectionAsync( SqliteConnection connection, CancellationToken cancellationToken) { // WAL is a persistent, database-level setting; re-applying it per connection // is cheap and a no-op once set. busy_timeout is per-connection state. await using SqliteCommand command = connection.CreateCommand(); command.CommandText = $"PRAGMA journal_mode=WAL; PRAGMA busy_timeout={(int)BusyTimeout.TotalMilliseconds};"; await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } }