gastownhall/beads · error

workspacegate: lock %s: %w

Error message

workspacegate: lock %s: %w

What it means

Acquire attempted a non-blocking flock (shared or exclusive) on the gate file and got an error that is neither lockfile.ErrLockBusy nor an IsLocked-style busy signal. Only genuine flock failures (bad fd, signal/interrupt, OS-level errors) reach this wrap; busy contention is handled separately by the retry loop. The file descriptor is closed before returning.

Source

Thrown at internal/workspacegate/gate.go:412

	try := lockfile.FlockSharedNonBlock
	if mode == Exclusive {
		try = lockfile.FlockExclusiveNonBlock
	}

	notified := false
	for {
		err := try(f)
		if err == nil {
			h := &Handle{gate: g, mode: mode, f: f}
			if mode == Exclusive {
				g.writeInfo(opts.Reason)
			}
			return h, nil
		}
		if !errors.Is(err, lockfile.ErrLockBusy) && !lockfile.IsLocked(err) {
			_ = f.Close()
			return nil, fmt.Errorf("workspacegate: lock %s: %w", g.path, err)
		}
		if !notified {
			notified = true
			if opts.OnWait != nil {
				opts.OnWait(g.busyDetail(mode))
			}
		}
		remaining := time.Until(deadline)
		if opts.Wait <= 0 || remaining <= 0 {
			_ = f.Close()
			return nil, fmt.Errorf("workspacegate: %s (%s mode) held by %s: %w",
				g.path, mode, g.busyDetail(mode), ErrBusy)
		}
		// Never sleep past the wait budget: a Wait shorter than the poll
		// interval must still come back within (about) Wait, and the
		// deadline is re-checked above before any further attempt.
		sleep := poll
		if remaining < sleep {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped error with errors.As(*os.SyscallError)/errno to identify the exact flock failure.
  2. Move the gate file onto a local filesystem (e.g. /tmp or the repo's .git dir) instead of NFS/network storage.
  3. Check for fd exhaustion (ulimit -n) if the error is EMFILE/ENOLCK.
  4. Retry the Acquire if the wrapped errno is transient (EINTR/ENOLCK under heavy load).
Defensive patterns

Strategy: retry

Try / catch

h, err := g.Acquire(ctx, mode, opts)
if err != nil {
    var se *os.SyscallError
    if errors.As(err, &se) && isTransientFlockErrno(se.Err) {
        // transient (EINTR/ENOLCK): back off and retry
        time.Sleep(100 * time.Millisecond)
        h, err = g.Acquire(ctx, mode, opts)
    }
    if err != nil {
        return err
    }
}
defer h.Close()

Prevention

When it happens

Trigger: Calling Acquire when lockfile.FlockSharedNonBlock or FlockExclusiveNonBlock returns a hard error other than busy, e.g. EINTR repeatedly, an invalid/closed file handle, or an OS that does not support flock on the filesystem (some network filesystems).

Common situations: Gate file on NFS or other filesystems where flock is unsupported or unreliable; fd exhaustion (EMFILE) preventing a usable handle; unusual kernel/security-policy interference (seccomp denying flock).

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/62627e393fef377b. Report an issue: GitHub.