fix(store-and-forward): re-seed queue-depth gauge from DB on sweep, clamp at 0 (review 2026-10-21 02/C3)
This commit is contained in:
@@ -283,6 +283,17 @@ public class StoreAndForwardService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private long _bufferedCount;
|
private long _bufferedCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sweeps run since the last gauge re-seed. Review 2026-10-21 02/C3: rows that arrive by
|
||||||
|
/// LocalDb CDC (standby) or are retried/parked/discarded on the peer never touch
|
||||||
|
/// <see cref="_bufferedCount"/>, so the seed taken at <see cref="StartAsync"/> drifts
|
||||||
|
/// (and goes negative after a failover). Every <see cref="GaugeReseedEverySweeps"/>th
|
||||||
|
/// sweep re-reads the Pending count from the database.
|
||||||
|
/// </summary>
|
||||||
|
private int _sweepsSinceReseed = GaugeReseedEverySweeps - 1; // first sweep re-seeds
|
||||||
|
|
||||||
|
private const int GaugeReseedEverySweeps = 6;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test seam: simulates a concurrent pre-seed
|
/// Test seam: simulates a concurrent pre-seed
|
||||||
/// <see cref="BufferAsync"/> increment landing on <see cref="_bufferedCount"/>
|
/// <see cref="BufferAsync"/> increment landing on <see cref="_bufferedCount"/>
|
||||||
@@ -477,7 +488,7 @@ public class StoreAndForwardService
|
|||||||
if (Interlocked.CompareExchange(ref _queueDepthProviderRegistered, 1, 0) == 0)
|
if (Interlocked.CompareExchange(ref _queueDepthProviderRegistered, 1, 0) == 0)
|
||||||
{
|
{
|
||||||
Interlocked.Add(ref _bufferedCount, pending);
|
Interlocked.Add(ref _bufferedCount, pending);
|
||||||
var provider = (Func<long>)(() => Interlocked.Read(ref _bufferedCount));
|
var provider = (Func<long>)(() => Math.Max(0, Interlocked.Read(ref _bufferedCount)));
|
||||||
_queueDepthProvider = provider;
|
_queueDepthProvider = provider;
|
||||||
ScadaBridgeTelemetry.SetQueueDepthProvider(provider);
|
ScadaBridgeTelemetry.SetQueueDepthProvider(provider);
|
||||||
}
|
}
|
||||||
@@ -831,10 +842,27 @@ public class StoreAndForwardService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ReseedBufferedCountAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (++_sweepsSinceReseed < GaugeReseedEverySweeps) return;
|
||||||
|
_sweepsSinceReseed = 0;
|
||||||
|
var pending = await _storage.GetMessageCountByStatusAsync(StoreAndForwardMessageStatus.Pending);
|
||||||
|
Interlocked.Exchange(ref _bufferedCount, Math.Max(0, pending));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "S&F queue-depth gauge re-seed failed; keeping the cached count");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RunSweepOwnedAsync()
|
private async Task RunSweepOwnedAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await ReseedBufferedCountAsync();
|
||||||
|
|
||||||
var gate = _deliveryGate;
|
var gate = _deliveryGate;
|
||||||
if (gate != null)
|
if (gate != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -97,6 +97,37 @@ public class QueueDepthGaugeTests : IAsyncLifetime, IDisposable
|
|||||||
return observed;
|
return observed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Review 2026-10-21 02/C3: rows that arrive without passing through this node's counter
|
||||||
|
/// paths (LocalDb CDC on a standby) are picked up by the sweep's periodic re-seed.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task Gauge_ReseedsFromDatabase_OnSweep()
|
||||||
|
{
|
||||||
|
Assert.Equal(0, ReadQueueDepthGauge());
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
await _storage.EnqueueAsync(new StoreAndForwardMessage
|
||||||
|
{
|
||||||
|
Id = $"cdc{i}",
|
||||||
|
Category = StoreAndForwardCategory.ExternalSystem,
|
||||||
|
Target = "t",
|
||||||
|
PayloadJson = "{}",
|
||||||
|
MaxRetries = 50,
|
||||||
|
RetryIntervalMs = 3_600_000,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
LastAttemptAt = DateTimeOffset.UtcNow,
|
||||||
|
Status = StoreAndForwardMessageStatus.Pending,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Assert.Equal(0, ReadQueueDepthGauge()); // counter never saw them
|
||||||
|
|
||||||
|
await _service.RetryPendingMessagesAsync();
|
||||||
|
|
||||||
|
Assert.Equal(3, ReadQueueDepthGauge());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that gauge, tracks buffered depth, across enqueue drain and park.</summary>
|
/// <summary>Verifies that gauge, tracks buffered depth, across enqueue drain and park.</summary>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user