gastownhall/beads · warning

workspacegate: unlock %s: %w

Error message

workspacegate: unlock %s: %w

What it means

Handle.Release removes the advisory sidecar, unlocks the flock, and joins any failures. If lockfile.FlockUnlock fails, the error is wrapped with the gate file path and joined into the handle's stored error. The lock itself is the authority, so an unlock failure usually means the descriptor is already invalid or the OS refused the unlock — the gate may still appear held to other processes until the process exits.

Source

Thrown at internal/workspacegate/gate.go:355

// file itself is intentionally never removed: deleting a lock file that
// another process is about to open reintroduces the split-inode race the
// gate location rules exist to avoid.
func (h *Handle) Release() error {
	if h == nil {
		return nil
	}
	h.once.Do(func() {
		var errs []error
		if h.mode == Exclusive {
			// Remove the sidecar BEFORE unlocking: after the unlock a new
			// exclusive holder may already have written its own sidecar,
			// and a late removal here would delete that holder's info.
			// Best effort; a leftover sidecar is ignored once the flock
			// is free.
			_ = os.Remove(h.gate.infoPath())
		}
		if err := lockfile.FlockUnlock(h.f); err != nil {
			errs = append(errs, fmt.Errorf("workspacegate: unlock %s: %w", h.gate.path, err))
		}
		if err := h.f.Close(); err != nil {
			errs = append(errs, fmt.Errorf("workspacegate: close %s: %w", h.gate.path, err))
		}
		h.err = errors.Join(errs...)
	})
	return h.err
}

// Acquire takes the gate in the given mode, polling until Options.Wait is
// exhausted or ctx is done. The returned handle's file descriptor is not
// inherited by spawned children (Go opens files close-on-exec on Unix and
// non-inheritable on Windows), so a dolt child outliving its bd parent
// does not keep the gate held.
func (g Gate) Acquire(ctx context.Context, mode Mode, opts Options) (*Handle, error) {
	if g.path == "" {
		return nil, errors.New("workspacegate: zero Gate; use ForWorkspace/ForPhysicalRoot")
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Do not close or otherwise touch the handle's internal file — Release is the single owner of the descriptor; call it exactly once.
  2. Inspect the wrapped OS error (errors.Unwrap / %w chain) to identify the syscall failure; on EINVAL the descriptor is likely already unlocked or invalid.
  3. If the gate appears stuck, verify no other path in the program unlocked the same flock; rely on Release's idempotent sync.Once rather than manual unlock.
  4. On filesystems without reliable advisory locks, move the workspace off network mounts — the package fails rather than degrades.

Example fix

// before
h.Close()          // closes underlying fd
h.Release()        // FlockUnlock now fails
// after
if err := h.Release(); err != nil {
    return fmt.Errorf("releasing gate: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before unwinding, ensure nothing else closed the handle's file.
// There is no pre-call check; rely on Release's idempotence.
if err := h.Release(); err != nil {
    var pe *fs.PathError
    if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EINVAL) {
        log.Printf("gate already unlocked")
    }
}

Try / catch

if err := h.Release(); err != nil {
    // Non-fatal in most cases: log and continue, but record it.
    log.Printf("workspacegate release: %v", err)
}

Prevention

When it happens

Trigger: Calling Release (directly or via MultiHandle.Release) after the underlying *os.File was already closed or otherwise invalidated elsewhere; OS-level failure of the unlock syscall (rare, e.g. EIO/EINVAL on a corrupted or unusual filesystem); double-release through a custom path that bypasses the sync.Once.

Common situations: Code that closes the handle's file separately before calling Release; running on a network filesystem with flaky advisory-lock support; a process unwinding after an I/O error.

Related errors


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