Skip to content

Commit 7aaea0c

Browse files
committed
Skip open circuit failure writes
Return early when recording a failure for a host whose circuit is already open. The state and failure history are unchanged in that case, so avoiding the CAS prevents redundant KV writes. #778 (comment) Assisted-by: Codex:gpt-5.5
1 parent 9f97b9b commit 7aaea0c

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

packages/fedify/src/federation/circuit-breaker.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ class AlwaysConflictingKvStore extends MemoryKvStore {
2424
}
2525
}
2626

27+
class CountingCasKvStore extends MemoryKvStore {
28+
attempts = 0;
29+
30+
override cas(
31+
key: KvKey,
32+
expectedValue: unknown,
33+
newValue: unknown,
34+
options?: KvStoreSetOptions,
35+
): Promise<boolean> {
36+
this.attempts++;
37+
return super.cas(key, expectedValue, newValue, options);
38+
}
39+
}
40+
2741
test("normalizeCircuitBreakerOptions() uses numeric failure policy", () => {
2842
const options = normalizeCircuitBreakerOptions({
2943
failureThreshold: 3,
@@ -468,6 +482,31 @@ test("CircuitBreaker bounds beforeSend CAS retries", async () => {
468482
});
469483
});
470484

485+
test("CircuitBreaker skips recording failures for open circuits", async () => {
486+
const kv = new CountingCasKvStore();
487+
const circuit = new CircuitBreaker({
488+
kv,
489+
prefix: ["_fedify", "circuit"],
490+
now: () => Temporal.Instant.from("2026-05-25T00:01:00Z"),
491+
});
492+
await kv.set(["_fedify", "circuit", "open.example"], {
493+
state: "open",
494+
failures: ["2026-05-25T00:00:00Z"],
495+
opened: "2026-05-25T00:00:00Z",
496+
});
497+
498+
assertEquals(await circuit.recordFailure("open.example"), undefined);
499+
assertEquals(kv.attempts, 0);
500+
assertEquals(
501+
await kv.get(["_fedify", "circuit", "open.example"]),
502+
{
503+
state: "open",
504+
failures: ["2026-05-25T00:00:00Z"],
505+
opened: "2026-05-25T00:00:00Z",
506+
},
507+
);
508+
});
509+
471510
test("CircuitBreaker prunes stale closed failure history", async () => {
472511
const kv = new MemoryKvStore();
473512
let now = Temporal.Instant.from("2026-05-25T00:00:00Z");

packages/fedify/src/federation/circuit-breaker.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,16 +336,15 @@ export class CircuitBreaker {
336336
const now = this.#now();
337337
for (let attempt = 0; attempt < 10; attempt++) {
338338
const oldState = await this.#get(remoteHost);
339+
if (oldState?.state === "open") return undefined;
339340
const oldFailures = oldState?.failures.map(Temporal.Instant.from) ?? [];
340341
const failures = this.#options.pruneFailures(
341342
[...oldFailures, now],
342343
now,
343344
);
344345
let newState: CircuitBreakerKvState;
345346
let transition: [CircuitBreakerState, CircuitBreakerState] | undefined;
346-
if (oldState?.state === "open") {
347-
newState = oldState;
348-
} else if (
347+
if (
349348
oldState?.state === "half-open" || this.#options.failure(failures)
350349
) {
351350
newState = {

0 commit comments

Comments
 (0)