{"record":{"id":"262aab348b4855c3","repo":"gastownhall/beads","slug":"workspacegate-acquiring-s-w","errorCode":null,"errorMessage":"workspacegate: acquiring %s: %w","messagePattern":"workspacegate: acquiring (.+?): %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workspacegate/gate.go","lineNumber":382,"sourceCode":"\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\n\t// signal context, and a nil-deref here kills the whole test binary.\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\tif err := ctx.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"workspacegate: acquiring %s: %w\", g.path, err)\n\t}\n\tpoll := opts.PollInterval\n\tif poll <= 0 {\n\t\tpoll = 100 * time.Millisecond\n\t}\n\tdeadline := time.Now().Add(opts.Wait)\n\n\tf, err := os.OpenFile(g.path, os.O_CREATE|os.O_RDWR, 0o600)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"workspacegate: open gate %s: %w\", g.path, err)\n\t}\n\n\ttry := lockfile.FlockSharedNonBlock\n\tif mode == Exclusive {\n\t\ttry = lockfile.FlockExclusiveNonBlock\n\t}\n\n\tnotified := false","sourceCodeStart":364,"sourceCodeEnd":400,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workspacegate/gate.go#L364-L400","documentation":"Gate.Acquire checks ctx.Err() before attempting the lock; if the context is already canceled or its deadline has expired, acquisition is refused immediately with the context error wrapped and the gate path prefixed. This is fail-fast: a dead context can never hold a gate, so no lock attempt is made.","triggerScenarios":"Calling g.Acquire (or AcquireAll) with a context that was already canceled, or whose deadline expired before the call — e.g. reusing a per-request context after the request finished, or a signal-triggered cancel from cobra's signal context firing before acquisition.","commonSituations":"CLI commands whose signal context was canceled by Ctrl-C racing startup; passing an http.Request's context to a background maintenance task that runs after the handler returns; a WithTimeout whose budget was consumed by earlier setup; unit tests passing a canceled context.","solutions":["Pass a live context — context.Background() (or a fresh signal context) for maintenance operations that must run to completion.","Check ctx.Err() before calling Acquire and create a fresh context if the old one is spent.","For background work, use context.WithoutCancel(ctx) (Go 1.21+) to drop cancellation while keeping values.","If a timeout is the cause, extend the WithTimeout budget to cover gate waiting (Options.Wait) plus the operation itself."],"exampleFix":"// before\nctx, cancel := context.WithTimeout(reqCtx, 5*time.Second)\ndefer cancel()\n// later, after handler returns:\nh, err := g.Acquire(ctx, workspacegate.Exclusive, opts) // ctx already done\n// after\nbg := context.WithoutCancel(reqCtx)\nh, err := g.Acquire(bg, workspacegate.Exclusive, opts)","handlingStrategy":"validation","validationCode":"if err := ctx.Err(); err != nil {\n    return fmt.Errorf(\"cannot acquire gate: %w\", err)\n}\nh, err := g.Acquire(ctx, workspacegate.Shared, workspacegate.Options{})","typeGuard":null,"tryCatchPattern":"h, err := g.Acquire(ctx, mode, opts)\nif err != nil {\n    if errors.Is(err, context.Canceled) {\n        return nil // caller canceled; clean exit\n    }\n    if errors.Is(err, context.DeadlineExceeded) {\n        return fmt.Errorf(\"gate wait budget exhausted: %w\", err)\n    }\n    return err\n}","preventionTips":["Never reuse an HTTP request or command context after its scope ends.","Use context.WithoutCancel for background maintenance that must finish.","Give WithTimeout budgets enough headroom for Options.Wait plus the operation.","Check ctx.Err() before long setup steps that precede Acquire."],"tags":["go","context","cancellation","locking","workspacegate"],"backgroundTag":"context-canceled","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}