{"record":{"id":"d97f15fb3ea7f58e","repo":"gastownhall/beads","slug":"workspacegate-close-s-w","errorCode":null,"errorMessage":"workspacegate: close %s: %w","messagePattern":"workspacegate: close (.+?): %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/workspacegate/gate.go","lineNumber":358,"sourceCode":"func (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}\n\t// Tolerate a nil context rather than panicking on ctx.Err below: gate\n\t// acquisition sits on CLI plumbing paths (cobra hooks, migrate helpers)\n\t// that tests and embedders invoke directly without the process-level","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workspacegate/gate.go#L340-L376","documentation":"Release finishes by closing the gate file descriptor; a close failure is wrapped with the gate path and joined (via errors.Join) alongside any unlock error into the handle's result. Close failures on a local file are rare but signal possible I/O problems (disk metadata, NFS issues) or a double-close of the descriptor.","triggerScenarios":"Calling Release when the descriptor was already closed elsewhere (EBADF); underlying storage errors during close (e.g. network filesystem); descriptor table pressure in extreme cases.","commonSituations":"Manual f.Close() calls on the handle's file; deferred closes duplicated with Release; NFS/network mounts with stale handles; long-running processes leaking fds until close misbehaves.","solutions":["Never close the handle's file yourself — call Release once and let its sync.Once manage unlock+close.","Check the joined error (errors.Is/As on the wrapped *fs.PathError) for EBADF, which means a double-close; remove the redundant close.","If the handle came from MultiHandle.Release, inspect the joined errors for each gate individually.","For repeated storage-level close errors, run filesystem checks or move the workspace to local storage."],"exampleFix":"// before\ndefer f.Close()\nh, _ := g.Acquire(ctx, mode, opts)\n...\nh.Release()\n// after\nh, _ := g.Acquire(ctx, mode, opts)\n...\nif err := h.Release(); err != nil {\n    log.Printf(\"gate release: %v\", err)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := h.Release(); err != nil {\n    if errors.Is(err, os.ErrClosed) {\n        return nil // already closed elsewhere; treat as done\n    }\n    return fmt.Errorf(\"gate close: %w\", err)\n}","preventionTips":["Never duplicate Close calls alongside Release.","Use defer h.Release() once per handle at the acquisition site.","Check the *fs.PathError errno (EBADF ⇒ double-close) to locate the offender.","Monitor fd leaks in long-running processes; fix leaks rather than suppressing close errors."],"tags":["go","file-locking","close","resource-cleanup","workspacegate"],"backgroundTag":"file-close-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}