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
- Wait 30s and retry — if a peer was just inspecting/reclaiming, the guard frees and acquisition proceeds.
- 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.
- Check for concurrent triggers (editor hooks, CI, two terminals) and serialize or dedupe analyze invocations on the same repo.
- On Linux/Windows prefer the default socket backend; do not force GITNEXUS_INDEX_LOCK_BACKEND=file unless sharing a mount across network namespaces.
- 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
- Deduplicate analyze triggers (one SessionStart hook per repo, not per session).
- Never manually delete analyze.lock.guard while any writer may run — use the RUNBOOK quiesced procedure.
- Keep the default socket backend on Linux/Windows; force file only for cross-netns shared mounts.
- Stagger CI/scheduled re-index jobs on the same repo.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timeout
- Cannot verify acquisition/reclaim guard ownership
- GitNexus: unable to acquire init lock after
- Guard and workload-lock cleanup failed
- Index lock verification failed
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)