{"record":{"id":"423bfa04f60b3f0f","repo":"gastownhall/beads","slug":"workspacegate-unlock-s-w","errorCode":null,"errorMessage":"workspacegate: unlock %s: %w","messagePattern":"workspacegate: unlock (.+?): %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/workspacegate/gate.go","lineNumber":355,"sourceCode":"// file itself is intentionally never removed: deleting a lock file that\n// another process is about to open reintroduces the split-inode race the\n// gate location rules exist to avoid.\nfunc (h *Handle) Release() error {\n\tif h == nil {\n\t\treturn nil\n\t}\n\th.once.Do(func() {\n\t\tvar errs []error\n\t\tif h.mode == Exclusive {\n\t\t\t// Remove the sidecar BEFORE unlocking: after the unlock a new\n\t\t\t// exclusive holder may already have written its own sidecar,\n\t\t\t// and a late removal here would delete that holder's info.\n\t\t\t// Best effort; a leftover sidecar is ignored once the flock\n\t\t\t// is free.\n\t\t\t_ = os.Remove(h.gate.infoPath())\n\t\t}\n\t\tif err := lockfile.FlockUnlock(h.f); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"workspacegate: unlock %s: %w\", h.gate.path, err))\n\t\t}\n\t\tif err := h.f.Close(); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"workspacegate: close %s: %w\", h.gate.path, err))\n\t\t}\n\t\th.err = errors.Join(errs...)\n\t})\n\treturn h.err\n}\n\n// Acquire takes the gate in the given mode, polling until Options.Wait is\n// exhausted or ctx is done. The returned handle's file descriptor is not\n// inherited by spawned children (Go opens files close-on-exec on Unix and\n// non-inheritable on Windows), so a dolt child outliving its bd parent\n// does not keep the gate held.\nfunc (g Gate) Acquire(ctx context.Context, mode Mode, opts Options) (*Handle, error) {\n\tif g.path == \"\" {\n\t\treturn nil, errors.New(\"workspacegate: zero Gate; use ForWorkspace/ForPhysicalRoot\")\n\t}","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workspacegate/gate.go#L337-L373","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Do not close or otherwise touch the handle's internal file — Release is the single owner of the descriptor; call it exactly once.","Inspect the wrapped OS error (errors.Unwrap / %w chain) to identify the syscall failure; on EINVAL the descriptor is likely already unlocked or invalid.","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.","On filesystems without reliable advisory locks, move the workspace off network mounts — the package fails rather than degrades."],"exampleFix":"// before\nh.Close()          // closes underlying fd\nh.Release()        // FlockUnlock now fails\n// after\nif err := h.Release(); err != nil {\n    return fmt.Errorf(\"releasing gate: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"// Before unwinding, ensure nothing else closed the handle's file.\n// There is no pre-call check; rely on Release's idempotence.\nif err := h.Release(); err != nil {\n    var pe *fs.PathError\n    if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EINVAL) {\n        log.Printf(\"gate already unlocked\")\n    }\n}","typeGuard":null,"tryCatchPattern":"if err := h.Release(); err != nil {\n    // Non-fatal in most cases: log and continue, but record it.\n    log.Printf(\"workspacegate release: %v\", err)\n}","preventionTips":["Treat Handle as sole owner of its file descriptor — never close it manually.","Call Release exactly once; it is idempotent via sync.Once.","Avoid network mounts lacking advisory-lock support for workspaces.","Log joined release errors in defer blocks instead of discarding them."],"tags":["go","file-locking","flock","release","workspacegate"],"backgroundTag":"flock-unlock-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}