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

  1. Wait for the owning process to finish and release the lock, then retry with a higher options.retries / retryDelayMs budget.
  2. Inspect the lock file's owner JSON (pid, hostname, processStartTime) and verify with `ps -p <pid>` whether the owner is alive on this host.
  3. If the owner is confirmed dead (pid not alive, or start time differs) and hostname matches, delete the lock file manually and retry.
  4. If the lock lives on a shared volume and hostname differs from yours, coordinate with the machine that created it — do not delete blindly.
  5. 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

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


AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-09-08). Data as JSON: /api/errors/71866c8dcc3304a0. Report an issue: GitHub.