fix(resilience): prevent circuit breaker stuck OPEN in combo path

Combo handlers call _onSuccess()/_onFailure() directly instead of
execute(), bypassing the OPEN→HALF_OPEN→CLOSED transition. After
resetTimeout elapses, canExecute() returns true and requests flow
through, but _onSuccess() only reset failureCount without changing
state — leaving the breaker permanently OPEN with 0 failures.

Handle OPEN state in both methods: _onSuccess() now transitions
OPEN→CLOSED, _onFailure() skips redundant re-transition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tombii
2026-04-02 22:36:57 +02:00
parent 03fb04f4c5
commit c0da968af2

View File

@@ -197,13 +197,20 @@ export class CircuitBreaker {
// ─── Internal Methods ────────────────────────
_onSuccess() {
if (this.state === STATE.HALF_OPEN) {
if (this.state === STATE.OPEN) {
// Direct call from combo path: timeout elapsed and request succeeded
// without going through execute(), so transition OPEN → CLOSED directly
this._transition(STATE.CLOSED);
this.failureCount = 0;
this.successCount = 0;
} else if (this.state === STATE.HALF_OPEN) {
this.successCount++;
this._transition(STATE.CLOSED);
this.failureCount = 0;
} else {
// In CLOSED state, just reset failure count
this.failureCount = 0;
}
// In CLOSED state, just reset failure count
this.failureCount = 0;
this._persistToDb();
}
@@ -211,7 +218,9 @@ export class CircuitBreaker {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === STATE.HALF_OPEN) {
if (this.state === STATE.OPEN) {
// Already OPEN — just update persistence (re-tripped by combo path)
} else if (this.state === STATE.HALF_OPEN) {
this._transition(STATE.OPEN);
} else if (this.failureCount >= this.failureThreshold) {
this._transition(STATE.OPEN);