charmbracelet/crush · warning

file lock is held by another process

Error message

file lock is held by another process

What it means

lock.ErrContended is returned by lock.TryFile when an advisory file lock is already held by another process. Unlike lock.File, which blocks until the lock is free or the context is cancelled, TryFile is the non-blocking variant: if the lock is contended it fails fast with this sentinel so callers can decide to skip or retry.

Source

Thrown at internal/lock/lock.go:30

// The lock file at path is created if it does not exist. It is never
// unlinked — flock is keyed by inode, not path, and unlinking could
// create a window where two processes lock different inodes at the
// same path.
//
// This is the canonical file-locking helper for Crush. Callers should
// prefer it over rolling their own platform-specific code.
package lock

import (
	"context"
	"errors"
	"fmt"
	"os"
)

// ErrContended is returned by TryFile when the lock is already held by
// another process.
var ErrContended = errors.New("file lock is held by another process")

// File acquires an exclusive advisory lock on the file at path, blocking
// until the lock is acquired or ctx is cancelled. It returns a release
// function that drops the lock and closes the underlying file descriptor.
//
// Pass a context with a deadline (e.g. context.WithTimeout) to bound the
// wait. Pass context.Background() to block indefinitely.
func File(ctx context.Context, path string) (func(), error) {
	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open lock file %q: %w", path, err)
	}

	release, err := lockFile(ctx, f)
	if err != nil {
		f.Close()
		return nil, err
	}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Exit the other running crush process that holds the data-dir lock, then retry.
  2. Use lock.File (blocking, context-bounded) instead of TryFile if waiting is acceptable.
  3. Point each concurrent instance at a different data directory, or verify no orphaned process still holds the lock (lsof on the lock file).

Example fix

// before
release, err := lock.TryFile(path)
if err != nil {
    return err // hard fail on contention
}
// after
release, err := lock.TryFile(path)
if errors.Is(err, lock.ErrContended) {
    return nil // another instance owns the lock; skip gracefully
}
if err != nil {
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Check whether another crush instance is running before acquiring
if _, err := os.Stat(lockPath); err == nil {
    if holders := processesHolding(lockPath); len(holders) > 0 { /* another instance alive */ }
}

Type guard

func isLockContended(err error) bool { return errors.Is(err, lock.ErrContended) }

Try / catch

release, err := lock.TryFile(path)
if errors.Is(err, lock.ErrContended) {
    return nil // or retry with backoff / fall back to lock.File
}
if err != nil {
    return err
}
defer release()

Prevention

When it happens

Trigger: Two crush processes starting simultaneously and both calling acquireDataDirLock/TryFile on the same data-dir lock file; TryFile against a lock held by a still-running previous instance.

Common situations: Launching a second crush instance while one is already running against the same project data dir; leftover processes after a crash still holding the fd; shared home/data directories across containers pointing at the same lock file.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/35e08ba98a5c058c. Report an issue: GitHub.