fix(store-and-forward): resolve StoreAndForward-006,007,008,009 — transactional parked reads, PipeTo, fault-isolated activity events; 002/011/012 deferred
This commit is contained in:
@@ -8,6 +8,19 @@ namespace ScadaLink.StoreAndForward;
|
||||
/// WP-9: SQLite persistence layer for store-and-forward messages.
|
||||
/// Uses direct Microsoft.Data.Sqlite (not EF Core) for lightweight site-side storage.
|
||||
/// No max buffer size per design decision.
|
||||
///
|
||||
/// StoreAndForward-008: every method opens a fresh <see cref="SqliteConnection"/> for
|
||||
/// the duration of the call rather than holding a long-lived connection. This is a
|
||||
/// deliberate trade-off, not an oversight: Microsoft.Data.Sqlite maintains an internal
|
||||
/// connection pool keyed on the connection string, so <c>OpenAsync</c> on a previously
|
||||
/// used connection string reuses a pooled handle instead of performing a real file
|
||||
/// open. The retry sweep therefore relies on that pool for acceptable performance —
|
||||
/// it calls <see cref="RemoveMessageAsync"/> / <see cref="UpdateMessageIfStatusAsync"/>
|
||||
/// once per due message, and with no max buffer size (by design) the buffer can grow
|
||||
/// large. The connection-per-call style keeps each method self-contained and
|
||||
/// transaction-scoped; if profiling ever shows the pooled open to be a bottleneck on
|
||||
/// the hot retry path, the remedy is a batched sweep API that opens one connection (and
|
||||
/// one transaction) per sweep.
|
||||
/// </summary>
|
||||
public class StoreAndForwardStorage
|
||||
{
|
||||
@@ -222,6 +235,12 @@ public class StoreAndForwardStorage
|
||||
|
||||
/// <summary>
|
||||
/// WP-12: Gets all parked messages, optionally filtered by category, with pagination.
|
||||
///
|
||||
/// StoreAndForward-006: the COUNT(*) and the paged SELECT run inside a single
|
||||
/// transaction so they observe one consistent snapshot. Without it, a concurrent
|
||||
/// enqueue/park/discard arriving between the two statements yields a TotalCount
|
||||
/// inconsistent with the returned page (flickering totals / off-by-one page math
|
||||
/// in the paginated UI).
|
||||
/// </summary>
|
||||
public async Task<(List<StoreAndForwardMessage> Messages, int TotalCount)> GetParkedMessagesAsync(
|
||||
StoreAndForwardCategory? category = null,
|
||||
@@ -231,8 +250,11 @@ public class StoreAndForwardStorage
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync();
|
||||
|
||||
// Count
|
||||
await using var countCmd = connection.CreateCommand();
|
||||
countCmd.Transaction = transaction;
|
||||
countCmd.CommandText = category.HasValue
|
||||
? "SELECT COUNT(*) FROM sf_messages WHERE status = @parked AND category = @category"
|
||||
: "SELECT COUNT(*) FROM sf_messages WHERE status = @parked";
|
||||
@@ -242,6 +264,7 @@ public class StoreAndForwardStorage
|
||||
|
||||
// Page
|
||||
await using var pageCmd = connection.CreateCommand();
|
||||
pageCmd.Transaction = transaction;
|
||||
var categoryFilter = category.HasValue ? " AND category = @category" : "";
|
||||
pageCmd.CommandText = $@"
|
||||
SELECT id, category, target, payload_json, retry_count, max_retries,
|
||||
@@ -257,6 +280,8 @@ public class StoreAndForwardStorage
|
||||
pageCmd.Parameters.AddWithValue("@offset", (pageNumber - 1) * pageSize);
|
||||
|
||||
var messages = await ReadMessagesAsync(pageCmd);
|
||||
|
||||
await transaction.CommitAsync();
|
||||
return (messages, totalCount);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user