Skip to content

Commit 1383bd8

Browse files
refactor(async/unstable): improve circuit breaker defaults and readability (#6917)
1 parent 1af87b2 commit 1383bd8

File tree

1 file changed

+35
-39
lines changed

1 file changed

+35
-39
lines changed

async/unstable_circuit_breaker.ts

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export interface CircuitBreakerOptions<T> {
1414
/**
1515
* Number of failures before opening the circuit.
1616
*
17+
* Note: For high-volume services, a low absolute threshold may cause
18+
* premature circuit opening during normal transient errors. Consider
19+
* a higher value proportional to your request volume, or combine with
20+
* a shorter {@linkcode failureWindowMs} to implement rate-based detection.
21+
*
1722
* @default {5}
1823
*/
1924
failureThreshold?: number;
@@ -35,7 +40,7 @@ export interface CircuitBreakerOptions<T> {
3540
/**
3641
* Maximum concurrent requests allowed in half-open state.
3742
*
38-
* @default {1}
43+
* @default {3}
3944
*/
4045
halfOpenMaxConcurrent?: number;
4146

@@ -296,7 +301,7 @@ export class CircuitBreaker<T = unknown> {
296301
failureThreshold = 5,
297302
cooldownMs = 30_000,
298303
successThreshold = 2,
299-
halfOpenMaxConcurrent = 1,
304+
halfOpenMaxConcurrent = 3,
300305
failureWindowMs = 60_000,
301306
isFailure = () => true,
302307
isResultFailure = () => false,
@@ -582,26 +587,23 @@ export class CircuitBreaker<T = unknown> {
582587
* OPEN → HALF_OPEN after cooldown expires.
583588
*/
584589
#resolveCurrentState(): CircuitBreakerState {
585-
const currentTime = Date.now();
590+
if (this.#state.state !== "open" || this.#state.openedAt === null) {
591+
return this.#state;
592+
}
586593

587-
// Auto-transition from OPEN to HALF_OPEN after cooldown
588-
if (
589-
this.#state.state === "open" &&
590-
this.#state.openedAt !== null
591-
) {
592-
const cooldownEnd = this.#state.openedAt + this.#cooldownMs;
593-
if (currentTime >= cooldownEnd) {
594-
const newState: CircuitBreakerState = {
595-
...this.#state,
596-
state: "half_open",
597-
consecutiveSuccesses: 0,
598-
halfOpenInFlight: 0,
599-
};
600-
this.#state = newState;
601-
this.#onStateChange?.("open", "half_open");
602-
}
594+
const cooldownEnd = this.#state.openedAt + this.#cooldownMs;
595+
if (Date.now() < cooldownEnd) {
596+
return this.#state;
603597
}
604598

599+
// Auto-transition from OPEN to HALF_OPEN after cooldown
600+
this.#state = {
601+
...this.#state,
602+
state: "half_open",
603+
consecutiveSuccesses: 0,
604+
halfOpenInFlight: 0,
605+
};
606+
this.#onStateChange?.("open", "half_open");
605607
return this.#state;
606608
}
607609

@@ -660,26 +662,20 @@ export class CircuitBreaker<T = unknown> {
660662

661663
/** Records a success and potentially closes the circuit from half-open. */
662664
#handleSuccess(previousState: CircuitState): void {
663-
if (previousState === "half_open") {
664-
const newSuccessCount = this.#state.consecutiveSuccesses + 1;
665-
666-
if (newSuccessCount >= this.#successThreshold) {
667-
// Recovered! Close the circuit
668-
this.#state = createInitialState();
669-
this.#onStateChange?.("half_open", "closed");
670-
this.#onClose?.();
671-
} else {
672-
this.#state = {
673-
...this.#state,
674-
consecutiveSuccesses: newSuccessCount,
675-
};
676-
}
677-
} else if (previousState === "closed") {
678-
// Reset consecutive success counter on success in closed state
679-
this.#state = {
680-
...this.#state,
681-
consecutiveSuccesses: 0,
682-
};
665+
if (previousState === "closed") {
666+
this.#state = { ...this.#state, consecutiveSuccesses: 0 };
667+
return;
668+
}
669+
670+
const newSuccessCount = this.#state.consecutiveSuccesses + 1;
671+
if (newSuccessCount >= this.#successThreshold) {
672+
// Recovered! Close the circuit
673+
this.#state = createInitialState();
674+
this.#onStateChange?.("half_open", "closed");
675+
this.#onClose?.();
676+
return;
683677
}
678+
679+
this.#state = { ...this.#state, consecutiveSuccesses: newSuccessCount };
684680
}
685681
}

0 commit comments

Comments
 (0)