From c0da968af299592a3ec73da5d36ac540c7701ca1 Mon Sep 17 00:00:00 2001 From: tombii Date: Thu, 2 Apr 2026 22:36:57 +0200 Subject: [PATCH] fix(resilience): prevent circuit breaker stuck OPEN in combo path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/shared/utils/circuitBreaker.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/shared/utils/circuitBreaker.ts b/src/shared/utils/circuitBreaker.ts index 4809bc4c7e..768178ed90 100644 --- a/src/shared/utils/circuitBreaker.ts +++ b/src/shared/utils/circuitBreaker.ts @@ -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);