From 1a53b8082a2c0dc778dade03b1a79519a59e48eb Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 8 Jul 2026 17:45:11 -0400 Subject: [PATCH] perf(store-and-forward): bound retry sweep with SweepBatchLimit (default 500) on the due-rows query --- .../StoreAndForwardOptions.cs | 7 +++++ .../StoreAndForwardService.cs | 2 +- .../StoreAndForwardStorage.cs | 9 ++++++- .../StoreAndForwardOptionsTests.cs | 4 +++ .../StoreAndForwardStorageTests.cs | 27 +++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs index b1f5927c..7546e942 100644 --- a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs @@ -22,4 +22,11 @@ public class StoreAndForwardOptions /// Interval for the background retry timer sweep. public TimeSpan RetryTimerInterval { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Maximum due rows loaded per retry sweep. Bounds sweep memory and duration + /// under backlog; the remainder is picked up on the next tick (10 s). + /// 0 = unlimited (legacy). + /// + public int SweepBatchLimit { get; set; } = 500; } diff --git a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs index 77527dd5..afeee811 100644 --- a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs @@ -601,7 +601,7 @@ public class StoreAndForwardService } } - var messages = await _storage.GetMessagesForRetryAsync(); + var messages = await _storage.GetMessagesForRetryAsync(_options.SweepBatchLimit); if (messages.Count == 0) return; _logger.LogDebug("Retry sweep: {Count} messages due for retry", messages.Count); diff --git a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs index 2348395b..b84a0347 100644 --- a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs +++ b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs @@ -180,7 +180,7 @@ public class StoreAndForwardStorage /// Gets all messages that are due for retry (Pending status, last attempt older than retry interval). /// /// A task that resolves to the list of messages due for retry, ordered by creation time ascending. - public async Task> GetMessagesForRetryAsync() + public async Task> GetMessagesForRetryAsync(int limit = 0) { await using var connection = new SqliteConnection(_connectionString); await connection.OpenAsync(); @@ -197,6 +197,13 @@ public class StoreAndForwardStorage OR (julianday('now') - julianday(last_attempt_at)) * 86400000 >= retry_interval_ms) ORDER BY created_at ASC"; + // Bound the sweep batch (oldest-first). 0 = unlimited (legacy). + if (limit > 0) + { + cmd.CommandText += "\n LIMIT @limit"; + cmd.Parameters.AddWithValue("@limit", limit); + } + cmd.Parameters.AddWithValue("@pending", (int)StoreAndForwardMessageStatus.Pending); return await ReadMessagesAsync(cmd); diff --git a/tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/StoreAndForwardOptionsTests.cs b/tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/StoreAndForwardOptionsTests.cs index 90f26f02..1da1c996 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/StoreAndForwardOptionsTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/StoreAndForwardOptionsTests.cs @@ -34,4 +34,8 @@ public class StoreAndForwardOptionsTests Assert.Equal(TimeSpan.FromMinutes(5), options.DefaultRetryInterval); Assert.Equal(100, options.DefaultMaxRetries); } + + [Fact] + public void SweepBatchLimit_Defaults_To_500() => + Assert.Equal(500, new StoreAndForwardOptions().SweepBatchLimit); } diff --git a/tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/StoreAndForwardStorageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/StoreAndForwardStorageTests.cs index ae66182f..e7d0f704 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/StoreAndForwardStorageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/StoreAndForwardStorageTests.cs @@ -587,6 +587,33 @@ public class StoreAndForwardStorageTests : IAsyncLifetime, IDisposable Assert.Null(retrieved!.ParentExecutionId); } + // ── Task 7 (arch review 02, Performance): bounded due-rows query ── + + [Fact] + public async Task GetMessagesForRetry_HonorsLimit_OldestFirst() + { + var baseTime = DateTimeOffset.UtcNow.AddMinutes(-10); + for (var i = 0; i < 5; i++) + { + await _storage.EnqueueAsync(new StoreAndForwardMessage + { + Id = $"m{i}", + Category = StoreAndForwardCategory.ExternalSystem, + Target = "t", + PayloadJson = "{}", + RetryCount = 0, + MaxRetries = 50, + RetryIntervalMs = 0, // always due + CreatedAt = baseTime.AddSeconds(i), + Status = StoreAndForwardMessageStatus.Pending + }); + } + + var page = await _storage.GetMessagesForRetryAsync(limit: 3); + Assert.Equal(3, page.Count); + Assert.Equal(new[] { "m0", "m1", "m2" }, page.Select(m => m.Id)); + } + private static StoreAndForwardMessage CreateMessage(string id, StoreAndForwardCategory category) { return new StoreAndForwardMessage