{"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":"warning","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/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus-shared/src/integrations/circuit-breaker.ts#L138-L174","documentation":"The worker pool's per-job idle timer fired, but the stall tracker shows the main thread was stalled (typically a GC pause near the heap limit) for at least STALL_CREDIT_FRACTION (0.5) of the job's timeout window. The worker's progress postMessages were starved by the blocked event loop, so the worker looked idle while actually healthy. The pool credits this once per job (stallCreditUsed) and re-arms the idle timer instead of requeueing or retiring the worker.","triggerScenarios":"Idle timeout (subBatchIdleTimeoutMs) fires on a job while stallTracker.read() - stallAtArm >= 0.5 * job.timeoutMs and the one-per-job stall credit is unused — classically a large relationship-batch reduce on the main thread driving GC while N workers stream progress messages that never get processed.","commonSituations":"Indexing very large repos on memory-constrained CI runners with the default Node heap; heap pressure from concurrent graph writes; machines under heavy CPU contention that stretch GC pauses past half the idle-timeout window. This warn is often the precursor to an OOM crash of the main process.","solutions":["Raise the Node heap for the analyze run: NODE_OPTIONS=\"--max-old-space-size=8192\" npx gitnexus analyze","Reduce worker parallelism so the main thread has headroom to drain progress messages (fewer workers, or run on a less loaded machine)","Lower GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES so main-thread reduction batches stay small","If it fires repeatedly on the same job, capture heap snapshots / run with --inspect to find the allocation source on the main thread"],"exampleFix":"# before\nnpx gitnexus analyze\n\n# after (give the main thread heap headroom)\nNODE_OPTIONS=\"--max-old-space-size=8192\" npx gitnexus analyze","handlingStrategy":"retry","validationCode":"// Before dispatching a heavy ingestion job, confirm main-thread heap headroom\nconst mu = process.memoryUsage();\nif (mu.heapUsed / mu.heapTotal > 0.85) {\n  // shed load: fewer workers, smaller sub-batches — GC stalls starve worker progress\n  process.env.GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES = String(2 * 1024 * 1024);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run analyze with an explicit --max-old-space-size sized to repo scale","Keep worker count at or below cores-1 on memory-tight CI runners","Watch for this warn as an early OOM signal and re-run with a larger heap rather than ignoring it"],"tags":["worker-pool","garbage-collection","timeout","ingestion"],"backgroundTag":"gc-pause-timeout","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}