charmbracelet/crush · error

acquire lock: %w

Error message

acquire lock: %w

What it means

In lockFile's retry loop, when Flock returns EWOULDBLOCK (lock held by another process), the function waits and retries until the passed context is done. When the context is cancelled or its deadline expires before the lock becomes free, it returns this error wrapping ctx.Err().

Source

Thrown at internal/lock/lock_unix.go:31

)

// retrySleep is the interval between non-blocking flock retries in the
// blocking File path. Small enough that contention resolution feels
// snappy; large enough that we don't burn a CPU spinning.
const retrySleep = 100 * time.Millisecond

func lockFile(ctx context.Context, f *os.File) (func(), error) {
	for {
		err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB)
		if err == nil {
			return func() { _ = unix.Flock(int(f.Fd()), unix.LOCK_UN) }, nil
		}
		if !errors.Is(err, unix.EWOULDBLOCK) {
			return nil, fmt.Errorf("flock: %w", err)
		}
		select {
		case <-ctx.Done():
			return nil, fmt.Errorf("acquire lock: %w", ctx.Err())
		case <-time.After(retrySleep):
		}
	}
}

func tryLockFile(f *os.File) (func(), error) {
	if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
		if errors.Is(err, unix.EWOULDBLOCK) {
			return nil, ErrContended
		}
		return nil, fmt.Errorf("flock: %w", err)
	}
	return func() { _ = unix.Flock(int(f.Fd()), unix.LOCK_UN) }, nil
}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Close or kill the other process holding the lock (check for running instances with the same data dir).
  2. Increase or remove the context deadline if a longer wait is acceptable (context.Background() blocks indefinitely).
  3. Delete a truly stale lock only after confirming the holder is dead (flock is released automatically on process exit, so a live flock implies a live holder).
  4. Fall back to lock.TryFile to surface contention immediately with ErrContended instead of waiting.

Example fix

// before
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
release, err := lock.File(ctx, lockPath) // expires under contention
// after
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
release, err := lock.File(ctx, lockPath)
if errors.Is(err, context.DeadlineExceeded) {
    return fmt.Errorf("another instance still holds %s", lockPath)
}
Defensive patterns

Strategy: retry

Validate before calling

// detect an active holder before waiting
tr, err := lock.TryFile(path)
if errors.Is(err, lock.ErrContended) {
    return fmt.Errorf("lock %s is held; resolve before waiting", path)
}

Try / catch

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
release, err := lock.File(ctx, path)
if errors.Is(err, context.DeadlineExceeded) {
    return fmt.Errorf("timed out waiting for lock held by another process")
}

Prevention

When it happens

Trigger: Calling lock.File with a context whose deadline expires while another process still holds the exclusive flock on the file.

Common situations: Two instances of the app started against the same data directory; the second one's bounded wait (context.WithTimeout) expires before the first releases the lock; debugging a hung session with an artificially short timeout.

Related errors


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