abhigyanpatwari/GitNexus · warning · FileLockBusyError
Lock is already held: ${lockPath}. Confirm no owner process
Error message
Lock is already held: ${lockPath}. Confirm no owner process is active, then remove it manually. What it means
FileLockBusyError is thrown by acquireFileLock when the lock file at lockPath is already held (fs.link fails with EEXIST), the holder is not reclaimable as stale, and the retry budget (options.retries) is exhausted. It signals that another live process owns the lock; the message advises confirming no owner is active and removing the file manually only in that case.
Source
Thrown at gitnexus/src/storage/file-lock.ts:77
try {
for (let attempt = 0; ; attempt += 1) {
try {
await fs.link(pendingPath, resolvedPath);
break;
} catch (error) {
if (!(await isLockConflict(error, resolvedPath))) throw error;
if (
await reclaimStaleLock(
resolvedPath,
owner,
options.isProcessAlive ?? isProcessAlive,
options.readProcessStartTime ?? readProcessStartTime,
)
) {
continue;
}
if (attempt >= retries) throw new FileLockBusyError(lockPath);
await sleep(retryDelayMs);
}
}
} finally {
// The lock is already published by now, but the release closure below is
// not yet in the caller's hands. Letting a staging-file cleanup error
// escape would strand a lock nobody can release, so prefer leaking the
// pending file — its name is per-acquisition, so it can never block anyone.
await fs.rm(pendingPath, { force: true }).catch(() => {});
}
let releasePromise: Promise<void> | undefined;
return () => (releasePromise ??= releaseOwnedLock(resolvedPath, owner.ownerId));
}
async function reclaimStaleLock(
lockPath: string,
guardOwner: FileLockOwner,View on GitHub (pinned to 0d1aed942f)
Solutions
- Wait for the owning process to finish and release the lock, then retry with a higher options.retries / retryDelayMs budget.
- Inspect the lock file's owner JSON (pid, hostname, processStartTime) and verify with `ps -p <pid>` whether the owner is alive on this host.
- If the owner is confirmed dead (pid not alive, or start time differs) and hostname matches, delete the lock file manually and retry.
- If the lock lives on a shared volume and hostname differs from yours, coordinate with the machine that created it — do not delete blindly.
- Check for orphaned processes (stray watcher/server) holding the lock and terminate them.
Example fix
// before
const release = await acquireFileLock(lockPath);
// after
const release = await acquireFileLock(lockPath, { retries: 10, retryDelayMs: 200 }); Defensive patterns
Strategy: retry
Validate before calling
import fs from 'node:fs/promises';
const owner = JSON.parse(await fs.readFile(`${lockPath}`, 'utf8'));
const stale = owner.hostname === os.hostname() && !isProcessAlive(owner.pid);
if (stale) await fs.rm(lockPath); Type guard
function isFileLockBusyError(e: unknown): e is FileLockBusyError {
return e instanceof FileLockBusyError;
} Try / catch
try {
const release = await acquireFileLock(lockPath, { retries: 10, retryDelayMs: 200 });
} catch (err) {
if (err instanceof FileLockBusyError) {
// inspect err.lockPath owner file; reclaim if provably stale, else surface to user
}
throw err;
} Prevention
- Always configure a sensible retries/retryDelayMs budget instead of the default (0 retries).
- Ensure processes release locks in a finally block so crashed-but-alive holders are rare.
- Run cooperating processes on the same host, or avoid shared-volume lock paths across hosts (stale detection refuses cross-host reclaim).
- Monitor for leftover .pending-* files and stale lock files and clean them in maintenance scripts after verifying the owner is dead.
- Keep lock paths unique per resource to avoid unrelated contention.
When it happens
Trigger: Calling acquireFileLock (directly or via acquireWatchLock, resetAutoSyncState, run, release, nextRelease) while another process holds the lock and either retries defaults to 0 or all retries are consumed; also a lock file left behind by a crashed process whose holder cannot be proven stale (different hostname, or PID alive with same start time per the check).
Common situations: Two GitNexus processes (CLI + watcher/server) contending for the same auto-sync lock; a previous run was SIGKILLed and the pid check cannot classify it stale (e.g. shared volume across hosts, hostname mismatch); long-held lock with retries too low.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- ${source}: branch name must not contain whitespace.
- GitNexus could not move the LadybugDB WAL sidecar at ${dbPat
- Timed out after ${waitedMs}ms waiting for another gitnexus a
- Timed out after ${waitedMs}ms waiting for another gitnexus a
- GitNexus: unable to acquire init lock after ${INIT_LOCK_MAX_
AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-09-08).
Data as JSON: /api/errors/71866c8dcc3304a0.
Report an issue: GitHub.