fix(r2-10): successful capability calls reset ConsecutiveFailures

This commit is contained in:
Joseph Doherty
2026-07-13 10:38:51 -04:00
parent 2cce61fc04
commit 0ba60db1e4
3 changed files with 68 additions and 4 deletions
@@ -235,6 +235,63 @@ public sealed class CapabilityInvokerTests
builder.CachedPipelineCount.ShouldBe(1, "two writes to the same host share one cached no-retry pipeline");
}
/// <summary>
/// R2-10: a successful capability call must reset <c>ConsecutiveFailures</c> so the counter
/// is a true "consecutive" gauge — nonzero only during an active retry storm, not stuck at the
/// last blip's count forever. Previously only the breaker's <c>OnClosed</c> reset it, so a
/// transient 2-retry blip that never opened the breaker read "2 failures" indefinitely.
/// </summary>
[Fact]
public async Task ExecuteAsync_OnSuccess_ResetsConsecutiveFailures()
{
var tracker = new DriverResilienceStatusTracker();
tracker.RecordFailure("drv-test", "host-1", DateTime.UtcNow);
tracker.RecordFailure("drv-test", "host-1", DateTime.UtcNow);
tracker.TryGet("drv-test", "host-1")!.ConsecutiveFailures.ShouldBe(2, "seeded failures");
var invoker = new CapabilityInvoker(
new DriverResiliencePipelineBuilder(),
"drv-test",
() => new DriverResilienceOptions { Tier = DriverTier.A },
statusTracker: tracker);
await invoker.ExecuteAsync(
DriverCapability.Read,
"host-1",
_ => ValueTask.FromResult(1),
CancellationToken.None);
tracker.TryGet("drv-test", "host-1")!.ConsecutiveFailures.ShouldBe(0,
"a successful capability call must clear the consecutive-failure counter");
}
/// <summary>
/// R2-10 companion: a successful non-idempotent write must also reset the consecutive-failure
/// counter on the caller's host (the write arm has its own success path).
/// </summary>
[Fact]
public async Task ExecuteWriteAsync_NonIdempotent_OnSuccess_ResetsConsecutiveFailures()
{
var tracker = new DriverResilienceStatusTracker();
tracker.RecordFailure("drv-test", "host-1", DateTime.UtcNow);
tracker.TryGet("drv-test", "host-1")!.ConsecutiveFailures.ShouldBe(1, "seeded failure");
var invoker = new CapabilityInvoker(
new DriverResiliencePipelineBuilder(),
"drv-test",
() => new DriverResilienceOptions { Tier = DriverTier.A },
statusTracker: tracker);
await invoker.ExecuteWriteAsync(
"host-1",
isIdempotent: false,
_ => ValueTask.FromResult(0),
CancellationToken.None);
tracker.TryGet("drv-test", "host-1")!.ConsecutiveFailures.ShouldBe(0,
"a successful non-idempotent write must clear the consecutive-failure counter");
}
/// <summary>
/// Core-009 regression — companion consistency assertion: the non-idempotent branch must
/// not observe two different option snapshots even if the accessor's returned value changes