perf(store-and-forward): parallel per-target sweep lanes (cap 4), sequential within lane
This commit is contained in:
@@ -686,4 +686,64 @@ public class StoreAndForwardServiceTests : IAsyncLifetime, IDisposable
|
||||
// assertion doesn't depend on created_at tie-breaking.
|
||||
Assert.Equal(new[] { 0, 1 }, new[] { r1, r2 }.OrderBy(x => x).ToArray());
|
||||
}
|
||||
|
||||
// ── Task 9 (arch review 02, Performance): parallel per-target lanes ──
|
||||
// A slow/dead target no longer serializes the whole sweep; delivery within a
|
||||
// single (category, target) lane stays sequential (per-target FIFO).
|
||||
|
||||
[Fact]
|
||||
public async Task RetrySweep_SlowTarget_DoesNotBlockOtherTargets()
|
||||
{
|
||||
var slowGate = new TaskCompletionSource();
|
||||
var healthyDelivered = new TaskCompletionSource();
|
||||
_service.RegisterDeliveryHandler(StoreAndForwardCategory.ExternalSystem, async msg =>
|
||||
{
|
||||
if (msg.Target == "slow") { await slowGate.Task; return true; }
|
||||
healthyDelivered.TrySetResult();
|
||||
return true;
|
||||
});
|
||||
await _service.EnqueueAsync(StoreAndForwardCategory.ExternalSystem, "slow", "{}",
|
||||
attemptImmediateDelivery: false, retryInterval: TimeSpan.Zero);
|
||||
await _service.EnqueueAsync(StoreAndForwardCategory.ExternalSystem, "healthy", "{}",
|
||||
attemptImmediateDelivery: false, retryInterval: TimeSpan.Zero);
|
||||
|
||||
var sweep = _service.RetryPendingMessagesAsync();
|
||||
// Healthy lane completes while the slow lane is still blocked — pre-fix this
|
||||
// times out because delivery is strictly serial across targets.
|
||||
await healthyDelivered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
slowGate.SetResult();
|
||||
await sweep;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RetrySweep_WithinTargetLane_StaysSequential()
|
||||
{
|
||||
var concurrent = 0;
|
||||
var maxConcurrent = 0;
|
||||
var delivered = 0;
|
||||
_service.RegisterDeliveryHandler(StoreAndForwardCategory.ExternalSystem, async _ =>
|
||||
{
|
||||
var c = Interlocked.Increment(ref concurrent);
|
||||
InterlockedMax(ref maxConcurrent, c);
|
||||
await Task.Delay(20);
|
||||
Interlocked.Decrement(ref concurrent);
|
||||
Interlocked.Increment(ref delivered);
|
||||
return true;
|
||||
});
|
||||
for (var i = 0; i < 4; i++)
|
||||
await _service.EnqueueAsync(StoreAndForwardCategory.ExternalSystem, "same-target", "{}",
|
||||
attemptImmediateDelivery: false, retryInterval: TimeSpan.Zero, messageId: $"m{i}");
|
||||
|
||||
await _service.RetryPendingMessagesAsync();
|
||||
|
||||
Assert.Equal(4, delivered);
|
||||
Assert.Equal(1, maxConcurrent); // one (category,target) lane: strictly sequential
|
||||
}
|
||||
|
||||
private static void InterlockedMax(ref int target, int value)
|
||||
{
|
||||
int current;
|
||||
do { current = Volatile.Read(ref target); if (value <= current) return; }
|
||||
while (Interlocked.CompareExchange(ref target, value, current) != current);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user