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
- Identify and close the other process holding the lock (Task Manager / handle inspection).
- Extend or drop the context deadline (context.Background() waits indefinitely).
- Use lock.TryFile when you want immediate ErrContended feedback instead of waiting.
- 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
- Use generous timeouts on Windows where server startup may be slow.
- Check for other running instances before long waits.
- Use TryFile for fail-fast user-facing checks and File for background waits.
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
- acquire lock: %w
- timed out after %s
- LockFileEx: %w
- session id missing from context
- agent message id missing from context
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/fa93c2332705baec.
Report an issue: GitHub.