{"record":{"id":"615b2b6e2cde5079","repo":"gastownhall/beads","slug":"workspacegate-s-s-mode-held-by-s-w","errorCode":null,"errorMessage":"workspacegate: %s (%s mode) held by %s: %w","messagePattern":"workspacegate: (.+?) \\((.+?) mode\\) held by (.+?): %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/workspacegate/gate.go","lineNumber":423,"sourceCode":"\t\t\tif mode == Exclusive {\n\t\t\t\tg.writeInfo(opts.Reason)\n\t\t\t}\n\t\t\treturn h, nil\n\t\t}\n\t\tif !errors.Is(err, lockfile.ErrLockBusy) && !lockfile.IsLocked(err) {\n\t\t\t_ = f.Close()\n\t\t\treturn nil, fmt.Errorf(\"workspacegate: lock %s: %w\", g.path, err)\n\t\t}\n\t\tif !notified {\n\t\t\tnotified = true\n\t\t\tif opts.OnWait != nil {\n\t\t\t\topts.OnWait(g.busyDetail(mode))\n\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","sourceCodeStart":405,"sourceCodeEnd":441,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workspacegate/gate.go#L405-L441","documentation":"Acquire exhausted its wait budget (opts.Wait <= 0 or the deadline passed) while the gate was still held by another process; it returns ErrBusy wrapped with details of the current holder (from the sidecar info written by writeInfo). This is the deliberate, expected timeout outcome of contention, not an internal failure.","triggerScenarios":"Calling Acquire with Exclusive (or Shared) while another process holds a conflicting flock and either opts.Wait is zero (non-blocking attempt) or the deadline elapses before the holder releases.","commonSituations":"Two commands (e.g. two bd/beads processes) operating on the same workspace concurrently; a stale process or crashed run left a long-lived holder; user scripts acquiring exclusive gates for long operations; parallel CI jobs sharing one checkout.","solutions":["Retry with a larger opts.Wait, or add your own backoff/retry around the Acquire call and check errors.Is(err, workspacegate.ErrBusy).","Read the busy detail in the error message to identify the holder (pid/command from the sidecar) and wait for it or kill it if stale.","Serialize work: use a queue or OnWait callback to surface contention rather than firing concurrent operations.","If the holder is a dead process on a local filesystem, the flock was already released; investigate who relaunches the conflicting command."],"exampleFix":"// before\nh, err := g.Acquire(ctx, workspacegate.Exclusive, workspacegate.AcquireOpts{})\n\n// after\nh, err := g.Acquire(ctx, workspacegate.Exclusive, workspacegate.AcquireOpts{\n    Wait:   30 * time.Second,\n    OnWait: func(d string) { log.Printf(\"gate busy: %s\", d) },\n})\nif errors.Is(err, workspacegate.ErrBusy) {\n    return fmt.Errorf(\"workspace in use; retry later: %w\", err)\n}","handlingStrategy":"retry","validationCode":"// pre-check before committing to a long wait\nheld, detail, err := g.ExclusiveHolder(ctx)\nif err == nil && held {\n    log.Printf(\"gate currently held: %s — deciding whether to wait\", detail)\n}","typeGuard":"func IsBusy(err error) bool { return errors.Is(err, workspacegate.ErrBusy) }","tryCatchPattern":"h, err := g.Acquire(ctx, mode, workspacegate.AcquireOpts{Wait: 30 * time.Second})\nif errors.Is(err, workspacegate.ErrBusy) {\n    // surface holder detail from err.Error(), schedule retry or abort\n    return fmt.Errorf(\"workspace busy: %w\", err)\n}","preventionTips":["Always set a sane opts.Wait and an OnWait callback to observe contention.","Check errors.Is(err, workspacegate.ErrBusy) to distinguish timeout from hard failure.","Serialize long operations through a queue instead of hammering the gate.","Use ExclusiveHolder to inspect stale holders before retrying."],"tags":["locking","timeout","contention","workspacegate"],"backgroundTag":"lock-timeout-busy","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}