perf(store-and-forward): enable WAL + per-connection busy_timeout on the S&F SQLite store

This commit is contained in:
Joseph Doherty
2026-07-08 21:01:53 -04:00
parent 7626d5ba34
commit 499a37ac78
2 changed files with 70 additions and 30 deletions
@@ -33,8 +33,18 @@ public class StoreAndForwardStorage
{
EnsureDatabaseDirectoryExists();
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
// Enable WAL so the concurrent writers this store has by design (script
// enqueues, the retry sweep's per-target lanes, standby replication applies,
// central pull queries) read/write without "database is locked". WAL is
// persistent + file-scoped; in-memory DBs report "memory" instead — harmless,
// so it is not asserted here.
await using (var walCmd = connection.CreateCommand())
{
walCmd.CommandText = "PRAGMA journal_mode=WAL";
await walCmd.ExecuteNonQueryAsync();
}
await using var command = connection.CreateCommand();
command.CommandText = @"
@@ -129,6 +139,23 @@ public class StoreAndForwardStorage
}
}
/// <summary>
/// Opens a connection with a 5s busy_timeout. Concurrent writers exist by
/// design (script enqueues, the sweep's lanes, standby replication applies,
/// central pull queries); with connection-per-operation the pragma must be
/// set per open (it is per-connection, and Microsoft.Data.Sqlite pooling
/// makes the extra statement cheap on a pooled physical connection).
/// </summary>
private async Task<SqliteConnection> OpenConnectionAsync()
{
var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var pragma = connection.CreateCommand();
pragma.CommandText = "PRAGMA busy_timeout = 5000";
await pragma.ExecuteNonQueryAsync();
return connection;
}
/// <summary>
/// Enqueues a new message with Pending status.
/// </summary>
@@ -136,8 +163,7 @@ public class StoreAndForwardStorage
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task EnqueueAsync(StoreAndForwardMessage message)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -189,8 +215,7 @@ public class StoreAndForwardStorage
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task UpsertMessageAsync(StoreAndForwardMessage message)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -243,8 +268,7 @@ public class StoreAndForwardStorage
/// <returns>A task that resolves to the list of messages due for retry, ordered by creation time ascending.</returns>
public async Task<List<StoreAndForwardMessage>> GetMessagesForRetryAsync(int limit = 0)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -277,8 +301,7 @@ public class StoreAndForwardStorage
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task UpdateMessageAsync(StoreAndForwardMessage message)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -312,8 +335,7 @@ public class StoreAndForwardStorage
StoreAndForwardMessage message,
StoreAndForwardMessageStatus expectedStatus)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -343,8 +365,7 @@ public class StoreAndForwardStorage
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task RemoveMessageAsync(string messageId)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = "DELETE FROM sf_messages WHERE id = @id";
@@ -365,8 +386,7 @@ public class StoreAndForwardStorage
int pageNumber = 1,
int pageSize = 50)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync();
@@ -411,8 +431,7 @@ public class StoreAndForwardStorage
/// <returns>A task that resolves to <c>true</c> if the message was found and reset to Pending; <c>false</c> if not found or not in Parked status.</returns>
public async Task<bool> RetryParkedMessageAsync(string messageId)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -435,8 +454,7 @@ public class StoreAndForwardStorage
/// <returns>A task that resolves to <c>true</c> if the message was found and deleted; <c>false</c> if not found or not in Parked status.</returns>
public async Task<bool> DiscardParkedMessageAsync(string messageId)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = "DELETE FROM sf_messages WHERE id = @id AND status = @parked";
@@ -453,8 +471,7 @@ public class StoreAndForwardStorage
/// <returns>A task that resolves to a dictionary mapping each category to its pending message count.</returns>
public async Task<Dictionary<StoreAndForwardCategory, int>> GetBufferDepthByCategoryAsync()
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -484,8 +501,7 @@ public class StoreAndForwardStorage
/// <returns>A task that resolves to the number of messages whose origin instance matches <paramref name="instanceName"/>.</returns>
public async Task<int> GetMessageCountByOriginInstanceAsync(string instanceName)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -504,8 +520,7 @@ public class StoreAndForwardStorage
/// <returns>A task that resolves to the matching message, or <c>null</c> if not found.</returns>
public async Task<StoreAndForwardMessage?> GetMessageByIdAsync(string messageId)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
@@ -526,8 +541,7 @@ public class StoreAndForwardStorage
/// <returns>A task that resolves to the number of messages currently in Parked status.</returns>
public async Task<int> GetParkedMessageCountAsync()
{
await using var conn = new SqliteConnection(_connectionString);
await conn.OpenAsync();
await using var conn = await OpenConnectionAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM sf_messages WHERE status = @parked";
cmd.Parameters.AddWithValue("@parked", (int)StoreAndForwardMessageStatus.Parked);
@@ -542,8 +556,7 @@ public class StoreAndForwardStorage
/// <returns>A task that resolves to the count of messages with the specified status.</returns>
public async Task<int> GetMessageCountByStatusAsync(StoreAndForwardMessageStatus status)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var connection = await OpenConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM sf_messages WHERE status = @status";