fix(r2-10): successful capability calls reset ConsecutiveFailures
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"tasks": [
|
||||
{ "id": "T1", "subject": "Tracker: LastBreakerClosedUtc + IsBreakerOpen + RecordBreakerClose (Core, TDD in DriverResilienceStatusTrackerTests)", "status": "completed", "blockedBy": [] },
|
||||
{ "id": "T2", "subject": "Builder: OnClosed records breaker-close on the tracker (TDD in DriverResiliencePipelineBuilderTests)", "status": "completed", "blockedBy": ["T1"] },
|
||||
{ "id": "T3", "subject": "Invoker: successful Execute* resets ConsecutiveFailures (TDD in CapabilityInvokerTests)", "status": "pending", "blockedBy": ["T1"] },
|
||||
{ "id": "T3", "subject": "Invoker: successful Execute* resets ConsecutiveFailures (TDD in CapabilityInvokerTests)", "status": "completed", "blockedBy": ["T1"] },
|
||||
{ "id": "T4", "subject": "Commons: DriverResilienceStatusChanged record + driver-resilience-status TopicName", "status": "pending", "blockedBy": [] },
|
||||
{ "id": "T5", "subject": "Host: DriverResilienceStatusPublisherService (BuildMessages mapping + 5s DPS timer shell; TDD mapping in Host.IntegrationTests)", "status": "pending", "blockedBy": ["T1", "T4"] },
|
||||
{ "id": "T6", "subject": "Host DI: register the publisher in AddOtOpcUaDriverFactories + ResilienceStatusReaderRegistrationTests wiring guard (tracker HAS a production reader)", "status": "pending", "blockedBy": ["T5"] },
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class CapabilityInvoker : IDriverCapabilityInvoker
|
||||
/// pipeline-invalidate can take effect without restarting the invoker.
|
||||
/// </param>
|
||||
/// <param name="driverType">Driver type name for structured-log enrichment (e.g. <c>"Modbus"</c>).</param>
|
||||
/// <param name="statusTracker">Optional resilience-status tracker. When wired, every capability call records start/complete so Admin <c>/hosts</c> can surface <see cref="ResilienceStatusSnapshot.CurrentInFlight"/> as the in-flight-call-depth proxy.</param>
|
||||
/// <param name="statusTracker">Optional resilience-status tracker. When wired, every capability call records start/complete so the AdminUI resilience panel can surface <see cref="ResilienceStatusSnapshot.CurrentInFlight"/> as the in-flight-call-depth proxy, and each successful call resets <see cref="ResilienceStatusSnapshot.ConsecutiveFailures"/> so the counter reflects only an active retry storm.</param>
|
||||
/// <param name="optionsGeneration">
|
||||
/// Options epoch for this invoker (01/S-7). Threaded into every pipeline-cache lookup so a lingering
|
||||
/// old child's late re-cache can never poison a newer invoker's pipeline. Defaults to 0 so existing
|
||||
@@ -81,7 +81,11 @@ public sealed class CapabilityInvoker : IDriverCapabilityInvoker
|
||||
{
|
||||
using (LogContextEnricher.Push(_driverInstanceId, _driverType, capability, LogContextEnricher.NewCorrelationId()))
|
||||
{
|
||||
return await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
|
||||
var result = await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
|
||||
// Success resets ConsecutiveFailures so the counter is a true "consecutive" gauge
|
||||
// (nonzero only during an active retry storm), not stuck at the last blip's count.
|
||||
_statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -106,6 +110,7 @@ public sealed class CapabilityInvoker : IDriverCapabilityInvoker
|
||||
using (LogContextEnricher.Push(_driverInstanceId, _driverType, capability, LogContextEnricher.NewCorrelationId()))
|
||||
{
|
||||
await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
|
||||
_statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -139,7 +144,9 @@ public sealed class CapabilityInvoker : IDriverCapabilityInvoker
|
||||
{
|
||||
using (LogContextEnricher.Push(_driverInstanceId, _driverType, DriverCapability.Write, LogContextEnricher.NewCorrelationId()))
|
||||
{
|
||||
return await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
|
||||
var result = await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
|
||||
_statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user