abhigyanpatwari/GitNexus · error · IndexLockTimeoutError

Timed out after ${waitedMs}ms waiting for another gitnexus a

Error message

Timed out after ${waitedMs}ms waiting for another gitnexus analyze (holder identity unknown) to release the index lock.

What it means

In the file-backend wait loop the lock file repeatedly vanished between this process's failed O_EXCL create and its read — other processes kept winning the lock, finishing, and unlinking it. This process starved until timeoutMs without ever reading a holder record, so the timeout reports an unknown holder (unknownHolder()). The loop is sleep-bounded by design; the failure means contention churn, not corruption.

Source

Thrown at gitnexus/src/storage/index-lock.ts:505

      if (Date.now() - lastDiagnosticAt >= DIAGNOSTIC_INTERVAL_MS) {
        lastDiagnosticAt = Date.now();
        if (waited >= DIAGNOSTIC_INTERVAL_MS) {
          opts.log?.(
            `Still waiting for analyze pid ${holder.pid} (${Math.round(waited / 1000)}s elapsed).`,
          );
        }
      }
      await sleep(jitteredDelay(pollMs, timeoutMs, waited));
      continue;
    }

    // holder === null: the lock file is either gone (vanished between the failed
    // create and our read) or present-but-unreadable (a crash between the
    // O_EXCL create and the record write, or a partial write). NEVER hot-loop
    // here — both branches are bounded by sleep + timeout.
    if (!existsSync(lockPath)) {
      malformedSince = null; // genuinely vanished → the next create likely wins
      if (waited >= timeoutMs) throw new IndexLockTimeoutError(unknownHolder(), waited, false);
      await sleep(jitteredDelay(pollMs, timeoutMs, waited));
      continue;
    }
    // Malformed orphan present. Reclaim only after a grace, so a live owner's
    // microsecond create→write window is never mistaken for a crash.
    if (malformedSince === null) malformedSince = Date.now();
    if (Date.now() - malformedSince >= malformedGraceMs(pollMs)) {
      opts.log?.('Reclaiming a malformed/partial index lock file (no readable owner record).');
      stealLock(lockPath, me, null); // reclaim ONLY while still unreadable; a live lock written since is left
      malformedSince = null;
      continue;
    }
    if (waited >= timeoutMs) throw new IndexLockTimeoutError(unknownHolder(), waited, false);
    await sleep(jitteredDelay(pollMs, timeoutMs, waited));
  }
};

/** Signals that the OS socket backend can't be used here (e.g. abstract

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Serialize analyze across all workers: shared CI lock, single scheduler, or staggered cron with jitter
  2. Retry later with backoff — the lock itself is healthy; only contention is the problem
  3. Raise GITNEXUS_INDEX_LOCK_TIMEOUT_MS so a queued run eventually gets its turn
  4. Index once into shared storage, or give each worker its own repo copy
Defensive patterns

Strategy: retry

Validate before calling

// orchestration-level: one analyze at a time per repo
await withMutex(`gitnexus:${repoKey}`, () => runAnalyze());

Try / catch

try {
  await runAnalyze();
} catch (e) {
  if (e?.name === 'IndexLockTimeoutError' && e.holder == null) {
    await sleep(5 * 60_000); // contention churn — back off and retry later
    return runAnalyze();
  }
  throw e;
}

Prevention

When it happens

Trigger: Many concurrent analyze processes racing on the same repo (CI matrix fan-out, immediate retry storms) such that this instance never wins the create race within the timeout window.

Common situations: A CI matrix where every shard runs analyze against the same checkout; schedulers retrying failed analyzes immediately; cron and webhooks firing analyze simultaneously.

Understand the failure class

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20). Data as JSON: /api/errors/15d36d9b5dd15214. Report an issue: GitHub.