fix(retry): charge gate budget on any repeated failure fingerprint (#460)
gateFailureFingerprints held one slot per gate, so the budget only charged when a failure repeated back to back. Whack-a-mole (fix A breaks B, fix B breaks A) alternates two fingerprints forever, every round read as progress, and the per-gate budget never triggered. Track the full set seen per gate and charge when the current fingerprint is a repeat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -83,7 +83,8 @@ class DefaultOrchestrationReducer : OrchestrationReducer {
|
||||
} else {
|
||||
state.gateRetryBudgets
|
||||
},
|
||||
gateFailureFingerprints = state.gateFailureFingerprints + (p.gate to p.fingerprint),
|
||||
gateFailureFingerprints = state.gateFailureFingerprints +
|
||||
(p.gate to ((state.gateFailureFingerprints[p.gate] ?: emptySet()) + p.fingerprint)),
|
||||
)
|
||||
|
||||
is RetrySalvageDecidedEvent -> if (p.decision == SalvageDecision.CONTINUE) {
|
||||
|
||||
@@ -26,9 +26,10 @@ class DefaultRetryCoordinator(
|
||||
policy: RetryPolicy,
|
||||
): RetryDecision {
|
||||
val fingerprint = FailureFingerprint.of(failureReason)
|
||||
val prevFingerprint = state.gateFailureFingerprints[gate]
|
||||
val progressed = prevFingerprint != fingerprint
|
||||
val charged = !progressed
|
||||
// Charge when this failure has been seen before for this gate — a repeat means the round
|
||||
// trip produced no state the gate has not already rejected. Comparing against only the
|
||||
// previous fingerprint misses cycles: A/B/A alternation reads as progress forever.
|
||||
val charged = fingerprint in (state.gateFailureFingerprints[gate] ?: emptySet())
|
||||
val currentCount = state.gateRetryBudgets[gate] ?: 0
|
||||
val newCount = if (charged) currentCount + 1 else currentCount
|
||||
val maxAttempts = policy.maxAttemptsFor(gate)
|
||||
|
||||
Reference in New Issue
Block a user