charmbracelet/crush · error

acquire lock: %w

Error message

acquire lock: %w

What it means

In the Windows lockFile retry loop, when LockFileEx reports contention (ERROR_LOCK_VIOLATION/ERROR_IO_PENDING) the function retries until the context is done. If the context is cancelled or its deadline lapses while the lock is still held by another process, this error wrapping ctx.Err() is returned.

Source

Thrown at internal/lock/lock_windows.go:37

func lockFile(ctx context.Context, f *os.File) (func(), error) {
	h := windows.Handle(f.Fd())
	for {
		ol := new(windows.Overlapped)
		flags := uint32(windows.LOCKFILE_EXCLUSIVE_LOCK | windows.LOCKFILE_FAIL_IMMEDIATELY)
		err := windows.LockFileEx(h, flags, 0, math.MaxUint32, math.MaxUint32, ol)
		if err == nil {
			return func() {
				ol := new(windows.Overlapped)
				_ = windows.UnlockFileEx(windows.Handle(f.Fd()), 0, math.MaxUint32, math.MaxUint32, ol)
			}, nil
		}
		if !errors.Is(err, windows.ERROR_LOCK_VIOLATION) && !errors.Is(err, windows.ERROR_IO_PENDING) {
			return nil, fmt.Errorf("LockFileEx: %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) {
	h := windows.Handle(f.Fd())
	ol := new(windows.Overlapped)
	flags := uint32(windows.LOCKFILE_EXCLUSIVE_LOCK | windows.LOCKFILE_FAIL_IMMEDIATELY)
	if err := windows.LockFileEx(h, flags, 0, math.MaxUint32, math.MaxUint32, ol); err != nil {
		if errors.Is(err, windows.ERROR_LOCK_VIOLATION) || errors.Is(err, windows.ERROR_IO_PENDING) {
			return nil, ErrContended
		}
		return nil, fmt.Errorf("LockFileEx: %w", err)
	}
	return func() {
		ol := new(windows.Overlapped)
		_ = windows.UnlockFileEx(windows.Handle(f.Fd()), 0, math.MaxUint32, math.MaxUint32, ol)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Identify and close the other process holding the lock (Task Manager / handle inspection).
  2. Extend or drop the context deadline (context.Background() waits indefinitely).
  3. Use lock.TryFile when you want immediate ErrContended feedback instead of waiting.
  4. Reboot if a zombie process holds the handle and cannot be identified.

Example fix

// before
release, err := lock.File(ctx, lockPath) // ctx has 1s timeout
// after
waitCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
release, err := lock.File(waitCtx, lockPath)
if errors.Is(err, context.DeadlineExceeded) {
    return fmt.Errorf("lock %s held by another process", lockPath)
}
Defensive patterns

Strategy: retry

Validate before calling

tr, err := lock.TryFile(path)
if errors.Is(err, lock.ErrContended) {
    return fmt.Errorf("another instance on this machine holds the lock")
}

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("lock %s still held after timeout", path)
}

Prevention

When it happens

Trigger: Calling lock.File on Windows with a bounded context while another process holds the exclusive LockFileEx lock for longer than the deadline.

Common situations: Second instance of the app started against the same data directory on Windows; the first instance crashed without releasing an OS lock is rare (Windows releases on handle close), so this usually means a genuinely running contender; too-short timeout.

Related errors


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