abhigyanpatwari/GitNexus · error · CircuitOpenError
Circuit '${key}' is open; retry in ${Math.ceil(retryAfterMs
Error message
Circuit '${key}' is open; retry in ${Math.ceil(retryAfterMs / 1000)}s What it means
Thrown by CircuitBreaker.check() when the breaker is in the 'open' state and the configured cooldown (default 30000ms) has not elapsed since it tripped. After failureThreshold (default 3) consecutive recordFailure() calls transition Closed→Open, the breaker fast-fails subsequent calls to protect the failing dependency instead of queuing load. The error's retryAfterMs field carries the remaining cooldown so callers can schedule a precise retry.
Source
Thrown at gitnexus-shared/src/integrations/circuit-breaker.ts:156
* remaining cooldown.
* 2. Open with cooldown elapsed AND a probe is already in flight
* (race: another caller transitioned to half-open and grabbed
* the permit on a microtask before us) → throws with
* `halfOpenRetryAfterMs`.
* 3. Half-Open with probe in flight → throws with `halfOpenRetryAfterMs`.
*
* **Pairing invariant**: every successful return from `check()` MUST
* be paired with exactly one `recordSuccess` / `recordFailure` /
* `recordNeutral` on every code path including thrown exceptions.
* Failing to pair leaves the probe permit consumed forever and
* wedges the breaker. See file-header JSDoc for the canonical
* try/finally pattern.
*/
check(): void {
if (this.state === 'open' && this.openedAt !== null) {
const elapsed = this.now() - this.openedAt;
if (elapsed < this.cooldownMs) {
throw new CircuitOpenError(this.cooldownMs - elapsed, this.key);
}
// Cooldown elapsed — transition to Half-Open. The very next
// `probeInFlight` check below decides whether THIS caller gets
// the permit or hits the gate.
this.state = 'half-open';
}
if (this.state === 'half-open') {
if (this.probeInFlight) {
throw new CircuitOpenError(this.halfOpenRetryAfterMs, this.key);
}
this.probeInFlight = true;
}
// Closed state falls through silently.
}
recordSuccess(): void {
this.probeInFlight = false;View on GitHub (pinned to d540b00184)
Solutions
- Wait for the cooldown (default 30s) to elapse; the breaker self-transitions to half-open on the next check()
- Verify the protected dependency is actually reachable — restart `gitnexus serve` or check the remote endpoint health
- In tests, call __resetBreakerRegistry__() in beforeEach to prevent breaker state leaking across cases
- Tune failureThreshold / cooldownMs via CircuitBreakerOptions if the defaults are too aggressive for your dependency's recovery profile
Example fix
// before — breaker trips, callers see CircuitOpenError immediately
const breaker = getBreaker('my-api');
breaker.check(); // throws if 3 prior failures within 30s
// after — honor retryAfterMs before retrying
try {
breaker.check();
} catch (e) {
if (e instanceof CircuitOpenError) {
await new Promise(r => setTimeout(r, e.retryAfterMs));
breaker.check();
} else throw e;
} Defensive patterns
Strategy: retry
Validate before calling
// Inspect breaker state without consuming the probe permit
import { getBreaker } from 'gitnexus-shared/src/integrations/circuit-breaker.js';
const breaker = getBreaker('my-key');
const state = breaker.getState(); // 'closed' | 'open' | 'half-open'
if (state === 'open') {
const openedAt = breaker.getOpenedAt();
const remaining = breaker.getCooldownMs() - (Date.now() - (openedAt ?? 0));
console.log(`breaker open, ~${Math.ceil(remaining/1000)}s left`);
} Type guard
import { CircuitOpenError } from 'gitnexus-shared/src/integrations/circuit-breaker.js';
function isCircuitOpen(e: unknown): e is CircuitOpenError {
return e instanceof CircuitOpenError;
} Try / catch
try {
breaker.check();
// ... protected op, paired with recordSuccess/recordFailure/recordNeutral
} catch (e) {
if (e instanceof CircuitOpenError) {
// e.retryAfterMs is the precise wait; schedule a retry, don't busy-loop
await new Promise(r => setTimeout(r, e.retryAfterMs));
return retry();
}
throw e;
} Prevention
- Always pair check() with exactly one record*() in a try/finally — an unpaired check wedges the probe permit
- Tune failureThreshold/cooldownMs to match the dependency's real recovery profile
- Key breakers per logical endpoint (getBreaker does this by host+pathname by default) so one bad endpoint doesn't poison others
- In tests, call __resetBreakerRegistry__() in beforeEach to avoid state leakage
When it happens
Trigger: Calling breaker.check() (directly or via resilientFetch) when state==='open', openedAt is non-null, and now()-openedAt < cooldownMs. Concretely: any resilientFetch against a breaker key that has already recorded 3 failures (default) within the cooldown window throws synchronously before fetch is even invoked.
Common situations: The protected backend or registry is down or returning 5xx/429 repeatedly — three failures tripped the breaker, so every call for the next 30s is rejected without hitting the network. Typical during local dev when `gitnexus serve` crashed, or against a flaky remote endpoint under load.
Related errors
- hf-circuit-open: HuggingFace download circuit is open after
- hf-circuit-open: HuggingFace download circuit opened after 3
- LLM endpoint circuit open: retry in ${Math.ceil(err.retryAft
- Circuit '${key}' is open; retry in ${Math.ceil(halfOpenRetry
- Request failed after retries (HTTP ${response.status})
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/6fd7df14c3dd39a4.
Report an issue: GitHub.