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