{"record":{"id":"6fd7df14c3dd39a4","repo":"abhigyanpatwari/GitNexus","slug":"circuit-key-is-open-retry-in-math-ceil-ret","errorCode":null,"errorMessage":"Circuit '${key}' is open; retry in ${Math.ceil(retryAfterMs / 1000)}s","messagePattern":"Circuit '(.+?)' is open; retry in (.+?)s","errorType":"exception","errorClass":"CircuitOpenError","httpStatus":null,"severity":"error","filePath":"gitnexus-shared/src/integrations/circuit-breaker.ts","lineNumber":156,"sourceCode":"   *      remaining cooldown.\n   *   2. Open with cooldown elapsed AND a probe is already in flight\n   *      (race: another caller transitioned to half-open and grabbed\n   *      the permit on a microtask before us) → throws with\n   *      `halfOpenRetryAfterMs`.\n   *   3. Half-Open with probe in flight → throws with `halfOpenRetryAfterMs`.\n   *\n   * **Pairing invariant**: every successful return from `check()` MUST\n   * be paired with exactly one `recordSuccess` / `recordFailure` /\n   * `recordNeutral` on every code path including thrown exceptions.\n   * Failing to pair leaves the probe permit consumed forever and\n   * wedges the breaker. See file-header JSDoc for the canonical\n   * try/finally pattern.\n   */\n  check(): void {\n    if (this.state === 'open' && this.openedAt !== null) {\n      const elapsed = this.now() - this.openedAt;\n      if (elapsed < this.cooldownMs) {\n        throw new CircuitOpenError(this.cooldownMs - elapsed, this.key);\n      }\n      // Cooldown elapsed — transition to Half-Open. The very next\n      // `probeInFlight` check below decides whether THIS caller gets\n      // the permit or hits the gate.\n      this.state = 'half-open';\n    }\n\n    if (this.state === 'half-open') {\n      if (this.probeInFlight) {\n        throw new CircuitOpenError(this.halfOpenRetryAfterMs, this.key);\n      }\n      this.probeInFlight = true;\n    }\n    // Closed state falls through silently.\n  }\n\n  recordSuccess(): void {\n    this.probeInFlight = false;","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/gitnexus-shared/src/integrations/circuit-breaker.ts#L138-L174","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before — breaker trips, callers see CircuitOpenError immediately\nconst breaker = getBreaker('my-api');\nbreaker.check(); // throws if 3 prior failures within 30s\n\n// after — honor retryAfterMs before retrying\ntry {\n  breaker.check();\n} catch (e) {\n  if (e instanceof CircuitOpenError) {\n    await new Promise(r => setTimeout(r, e.retryAfterMs));\n    breaker.check();\n  } else throw e;\n}","handlingStrategy":"retry","validationCode":"// Inspect breaker state without consuming the probe permit\nimport { getBreaker } from 'gitnexus-shared/src/integrations/circuit-breaker.js';\nconst breaker = getBreaker('my-key');\nconst state = breaker.getState(); // 'closed' | 'open' | 'half-open'\nif (state === 'open') {\n  const openedAt = breaker.getOpenedAt();\n  const remaining = breaker.getCooldownMs() - (Date.now() - (openedAt ?? 0));\n  console.log(`breaker open, ~${Math.ceil(remaining/1000)}s left`);\n}","typeGuard":"import { CircuitOpenError } from 'gitnexus-shared/src/integrations/circuit-breaker.js';\nfunction isCircuitOpen(e: unknown): e is CircuitOpenError {\n  return e instanceof CircuitOpenError;\n}","tryCatchPattern":"try {\n  breaker.check();\n  // ... protected op, paired with recordSuccess/recordFailure/recordNeutral\n} catch (e) {\n  if (e instanceof CircuitOpenError) {\n    // e.retryAfterMs is the precise wait; schedule a retry, don't busy-loop\n    await new Promise(r => setTimeout(r, e.retryAfterMs));\n    return retry();\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["circuit-breaker","resilience","network","fail-fast"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}