{"record":{"id":"cb19f890268b5dbc","repo":"gastownhall/beads","slug":"workspacegate-waiting-for-s-w","errorCode":null,"errorMessage":"workspacegate: waiting for %s: %w","messagePattern":"workspacegate: waiting for (.+?): %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/workspacegate/gate.go","lineNumber":436,"sourceCode":"\t\t\t}\n\t\t}\n\t\tremaining := time.Until(deadline)\n\t\tif opts.Wait <= 0 || remaining <= 0 {\n\t\t\t_ = f.Close()\n\t\t\treturn nil, fmt.Errorf(\"workspacegate: %s (%s mode) held by %s: %w\",\n\t\t\t\tg.path, mode, g.busyDetail(mode), ErrBusy)\n\t\t}\n\t\t// Never sleep past the wait budget: a Wait shorter than the poll\n\t\t// interval must still come back within (about) Wait, and the\n\t\t// deadline is re-checked above before any further attempt.\n\t\tsleep := poll\n\t\tif remaining < sleep {\n\t\t\tsleep = remaining\n\t\t}\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\t_ = f.Close()\n\t\t\treturn nil, fmt.Errorf(\"workspacegate: waiting for %s: %w\", g.path, ctx.Err())\n\t\tcase <-time.After(sleep):\n\t\t}\n\t}\n}\n\n// writeInfo records the advisory exclusive-holder sidecar. Failures are\n// deliberately swallowed: diagnostics must never block the operation that\n// already holds the authoritative lock. The write goes to an O_EXCL temp\n// file renamed into place, which (a) never follows a pre-planted symlink\n// at either path — plain WriteFile would truncate the symlink's target —\n// and (b) is atomic, so concurrent readers cannot see torn JSON.\nfunc (g Gate) writeInfo(reason string) {\n\thost, _ := os.Hostname()\n\tdata, err := json.Marshal(Info{\n\t\tPID:       os.Getpid(),\n\t\tHostname:  host,\n\t\tReason:    reason,\n\t\tStartedAt: time.Now().UTC(),","sourceCodeStart":418,"sourceCodeEnd":454,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workspacegate/gate.go#L418-L454","documentation":"While Acquire was sleeping between lock retries, its context was cancelled or timed out; it wraps ctx.Err() with the gate path. The file descriptor is closed and no lock is held. This reports why the wait was abandoned, not a lock failure.","triggerScenarios":"Calling Acquire with a ctx that is cancelled, or whose deadline/timeout expires, while the gate is busy and the function is inside the select on ctx.Done() between retry polls.","commonSituations":"HTTP request contexts timing out while waiting on a long-held gate; CLI processes interrupted (SIGINT) cancelling the context; a parent goroutine cancelling work because an earlier step failed; overly short context deadlines relative to opts.Wait.","solutions":["Check errors.Is(err, context.DeadlineExceeded) vs context.Canceled to decide whether to extend the deadline or treat it as user abort.","Increase the context timeout/deadline to exceed the expected holder duration.","Ensure the context stays alive for the whole Acquire (do not tie it to a short-lived request if the wait can be long).","Use the OnWait callback to log contention early so operators can release the gate before the context dies."],"exampleFix":"// before\nh, err := g.Acquire(ctx, workspacegate.Exclusive, workspacegate.AcquireOpts{Wait: time.Minute})\n\n// after\nacqCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)\ndefer cancel()\nh, err := g.Acquire(acqCtx, workspacegate.Exclusive, workspacegate.AcquireOpts{Wait: time.Minute})","handlingStrategy":"try-catch","validationCode":"if ctx.Err() != nil {\n    return fmt.Errorf(\"context already done before acquire: %w\", ctx.Err())\n}\nif dl, ok := ctx.Deadline(); ok && time.Until(dl) < expectedHolderTime {\n    // widen the deadline before attempting\n}","typeGuard":null,"tryCatchPattern":"h, err := g.Acquire(ctx, mode, opts)\nif err != nil {\n    switch {\n    case errors.Is(err, context.Canceled):\n        return fmt.Errorf(\"acquire aborted by caller\")\n    case errors.Is(err, context.DeadlineExceeded):\n        return fmt.Errorf(\"acquire wait budget exhausted: %w\", err)\n    default:\n        return err\n    }\n}","preventionTips":["Set context deadlines longer than opts.Wait so cancellation is intentional, not accidental.","Keep the acquire context independent of short-lived request contexts.","Handle SIGINT by cancelling the context so the error path is deterministic.","Use the OnWait hook to release/abort early before the context dies."],"tags":["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"}