Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions async/unstable_circuit_breaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,8 +758,7 @@ export class CircuitBreaker<T = unknown> {
const failureCount = this.#failures.total;

const shouldOpen = previousState === "half_open" ||
(totalRequests >= this.#minimumThroughput &&
failureCount / totalRequests >= this.#failureRateThreshold);
this.#exceedsFailureRate();

const existingOpenedAt = this.#state.state === "open"
? this.#state.openedAt
Expand Down Expand Up @@ -790,9 +789,33 @@ export class CircuitBreaker<T = unknown> {
}
}

/** Records a success and potentially closes the circuit from half-open. */
/** Whether the window has enough requests and the rate meets the threshold. */
#exceedsFailureRate(): boolean {
const totalRequests = this.#requests.total;
return totalRequests >= this.#minimumThroughput &&
this.#failures.total / totalRequests >= this.#failureRateThreshold;
}

/**
* Records a success. Closes the circuit from half-open once enough
* successes accrue. In closed state, a success can still be the request
* that lifts the window over `minimumThroughput`, so the rate is evaluated.
*/
#handleSuccess(previousState: CircuitState): void {
if (previousState === "closed") return;
if (previousState === "closed") {
if (this.#state.state !== "closed" || !this.#exceedsFailureRate()) {
return;
}
this.#state = {
...this.#state,
state: "open",
openedAt: Date.now(),
consecutiveSuccesses: 0,
};
this.#onStateChange?.("closed", "open");
this.#onOpen?.(this.#failures.total, this.#requests.total);
return;
}
if (this.#state.state !== "half_open") return;

const newSuccessCount = this.#state.consecutiveSuccesses + 1;
Expand Down
30 changes: 30 additions & 0 deletions async/unstable_circuit_breaker_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,36 @@ Deno.test("CircuitBreaker.execute() frees half_open concurrency slot on failure"
assertEquals(breaker.state, "closed");
});

Deno.test("CircuitBreaker.execute() opens when a success brings the window to minimumThroughput", async () => {
const opens: [number, number][] = [];
const changes: string[] = [];
const breaker = new CircuitBreaker({
failureRateThreshold: 0.5,
minimumThroughput: 3,
onOpen: (failures, requests) => opens.push([failures, requests]),
onStateChange: (from, to) => changes.push(`${from}->${to}`),
});

await failN(breaker, 2);
assertEquals(breaker.state, "closed");

await succeedN(breaker, 1);
assertEquals(breaker.state, "open");
assertEquals(opens, [[2, 3]]);
assertEquals(changes, ["closed->open"]);
});

Deno.test("CircuitBreaker.execute() stays closed when a success keeps the rate below the threshold", async () => {
const breaker = new CircuitBreaker({
failureRateThreshold: 0.5,
minimumThroughput: 3,
});

await failN(breaker, 1);
await succeedN(breaker, 2);
assertEquals(breaker.state, "closed");
});

Deno.test("CircuitBreaker.execute() prevents stale half_open success from closing after concurrent failure", async () => {
using time = new FakeTime();

Expand Down
Loading