abhigyanpatwari/GitNexus · error · IndexLockTimeoutError

Index lock acquisition timed out

Error message

Index lock acquisition timed out

What it means

This IndexLockTimeoutError is thrown by the file-backend lock when the O_EXCL acquisition/reclaim guard file (<analyze.lock>.guard) cannot be created because another process keeps holding it. Guard contention is capped at 30 seconds (GUARD_TIMEOUT_MS) regardless of the overall workload wait budget, because a guard orphan is never automatically taken over — failing closed is deliberate. The holder identity is unknown (holderKnown=false, pid -1) because a guard tells us nothing about who created it.

Solutions

  1. Wait 30s and retry — if a peer was just inspecting/reclaiming, the guard frees and acquisition proceeds.
  2. Quiesce all gitnexus analyze processes (check for live pids named in earlier logs), then follow RUNBOOK.md quiesced recovery to manually remove the orphaned guard file.
  3. Check for concurrent triggers (editor hooks, CI, two terminals) and serialize or dedupe analyze invocations on the same repo.
  4. On Linux/Windows prefer the default socket backend; do not force GITNEXUS_INDEX_LOCK_BACKEND=file unless sharing a mount across network namespaces.
  5. If guard contention is chronic, stagger scheduled re-index jobs or use a repo-level queue.

Example fix

// before (two hooks race on the same repo)
// hook: npx gitnexus analyze --index-only  (both fire on session start)

// after (serialize with an OS-level wrapper)
// hook: flock /tmp/gitnexus-$(pwd | md5sum | cut -c1-8).lock npx gitnexus analyze --index-only
Defensive patterns

Strategy: retry

Validate before calling

// Before triggering analyze, check for an orphaned guard with no live writers:
import { existsSync, readFileSync } from 'node:fs';
const guard = '.gitnexus/analyze.lock.guard';
if (existsSync(guard)) {
  const rec = JSON.parse(readFileSync(guard, 'utf8'));
  console.warn(`guard present from pid ${rec.pid} on ${rec.hostname} — ensure no live analyze before recovery`);
}

Try / catch

import { acquireIndexLock, isIndexLockGuardTimeout } from 'gitnexus/.../index-lock.js';
try {
  await acquireIndexLock(lockDir);
} catch (err) {
  if (isIndexLockGuardTimeout(err)) {
    // err.guardPath set; quiesce writers, then retry after a backoff
    await sleep(30_000);
    return acquireIndexLock(lockDir);
  }
  throw err;
}

Prevention

When it happens

Trigger: acquireIndexLock (file backend) polls openSync(guardPath, 'wx') and keeps getting EEXIST or Windows delete-pending EPERM for 30+ seconds while a peer writer holds the inspect guard; remainingGuardCreateWaitMs then throws with guardPath set.

Common situations: Two editor/agent SessionStart hooks firing analyze on the same macOS/BSD repo (file backend platform) at once; a crashed analyze that left an orphaned analyze.lock.guard on a non-Linux host; many waiters queueing behind a long peer's guard-held inspection window; GITNEXUS_INDEX_LOCK_BACKEND=file forced in containers sharing a mount.

Understand the failure class

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@ac9a4e9abd (2026-09-15). Data as JSON: /api/errors/f7556ba30143f88f. Report an issue: GitHub.

Appendix: source

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

  now: number;
  startedAt: number;
  timeoutMs: number;
  guardWaitSince: number;
  permissionDeadline: number;
  permissionError: unknown;
  code: string | undefined;
  err: unknown;
  guardPath: string;
  lastLiveHolder: LockRecord | null;
}): number => {
  const workloadDeadline = args.startedAt + args.timeoutMs;
  const guardDeadline = args.guardWaitSince + GUARD_TIMEOUT_MS;
  const deadline = Math.min(workloadDeadline, guardDeadline, args.permissionDeadline);
  if (args.now < deadline) return deadline - args.now;
  if (args.permissionError && args.now >= args.permissionDeadline) throw args.permissionError;
  if (args.code === 'EPERM') throw args.err;
  if (args.now >= guardDeadline) {
    throw new IndexLockTimeoutError(
      unknownHolder(),
      args.now - args.startedAt,
      false,
      args.guardPath,
    );
  }
  const remembered = args.lastLiveHolder ?? unknownHolder();
  throw new IndexLockTimeoutError(
    remembered,
    args.now - args.startedAt,
    args.lastLiveHolder !== null,
  );
};

/** Drop this attempt's guard. A throw here discards the pending handle. */
const releaseAcquisitionGuard = (
  guardPath: string,
  me: LockRecord,

View on GitHub (pinned to ac9a4e9abd)