{"record":{"id":"f7556ba30143f88f","repo":"abhigyanpatwari/GitNexus","slug":"index-lock-acquisition-timed-out","errorCode":null,"errorMessage":"Index lock acquisition timed out","messagePattern":"Index lock acquisition timed out","errorType":"exception","errorClass":"IndexLockTimeoutError","httpStatus":null,"severity":"error","filePath":"gitnexus/src/storage/index-lock.ts","lineNumber":329,"sourceCode":"  now: number;\n  startedAt: number;\n  timeoutMs: number;\n  guardWaitSince: number;\n  permissionDeadline: number;\n  permissionError: unknown;\n  code: string | undefined;\n  err: unknown;\n  guardPath: string;\n  lastLiveHolder: LockRecord | null;\n}): number => {\n  const workloadDeadline = args.startedAt + args.timeoutMs;\n  const guardDeadline = args.guardWaitSince + GUARD_TIMEOUT_MS;\n  const deadline = Math.min(workloadDeadline, guardDeadline, args.permissionDeadline);\n  if (args.now < deadline) return deadline - args.now;\n  if (args.permissionError && args.now >= args.permissionDeadline) throw args.permissionError;\n  if (args.code === 'EPERM') throw args.err;\n  if (args.now >= guardDeadline) {\n    throw new IndexLockTimeoutError(\n      unknownHolder(),\n      args.now - args.startedAt,\n      false,\n      args.guardPath,\n    );\n  }\n  const remembered = args.lastLiveHolder ?? unknownHolder();\n  throw new IndexLockTimeoutError(\n    remembered,\n    args.now - args.startedAt,\n    args.lastLiveHolder !== null,\n  );\n};\n\n/** Drop this attempt's guard. A throw here discards the pending handle. */\nconst releaseAcquisitionGuard = (\n  guardPath: string,\n  me: LockRecord,","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/ac9a4e9abd8fd3058c070b72c23402a4f887929a/gitnexus/src/storage/index-lock.ts#L311-L347","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (two hooks race on the same repo)\n// hook: npx gitnexus analyze --index-only  (both fire on session start)\n\n// after (serialize with an OS-level wrapper)\n// hook: flock /tmp/gitnexus-$(pwd | md5sum | cut -c1-8).lock npx gitnexus analyze --index-only","handlingStrategy":"retry","validationCode":"// Before triggering analyze, check for an orphaned guard with no live writers:\nimport { existsSync, readFileSync } from 'node:fs';\nconst guard = '.gitnexus/analyze.lock.guard';\nif (existsSync(guard)) {\n  const rec = JSON.parse(readFileSync(guard, 'utf8'));\n  console.warn(`guard present from pid ${rec.pid} on ${rec.hostname} — ensure no live analyze before recovery`);\n}","typeGuard":null,"tryCatchPattern":"import { acquireIndexLock, isIndexLockGuardTimeout } from 'gitnexus/.../index-lock.js';\ntry {\n  await acquireIndexLock(lockDir);\n} catch (err) {\n  if (isIndexLockGuardTimeout(err)) {\n    // err.guardPath set; quiesce writers, then retry after a backoff\n    await sleep(30_000);\n    return acquireIndexLock(lockDir);\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["filesystem","locking","concurrency","timeout"],"backgroundTag":"index-lock-guard-timeout","analyzedSha":"ac9a4e9abd8fd3058c070b72c23402a4f887929a","analyzedAt":"2026-09-15T23:29:44.066Z","contentChangedAt":"2026-09-15T23:29:44.066Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}