{"record":{"id":"8cd41c8bd7330d29","repo":"gastownhall/beads","slug":"errlockheld","errorCode":"ErrLockHeld","errorMessage":"proxy lock held by another proxy on this rootDir","messagePattern":"proxy lock held by another proxy on this rootDir","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"info","filePath":"internal/storage/dbproxy/proxy/server.go","lineNumber":80,"sourceCode":"\tconns       errgroup.Group\n}\n\nconst (\n\tPIDFileName  = \"proxy.pid\"\n\tLogFileName  = \"proxy.log\"\n\tLockFileName = \"proxy.lock\"\n)\n\n// LockHeldExitCode is the exit code a child proxy should use when\n// ListenAndServe returns ErrLockHeld. The spawning parent treats this\n// (EX_TEMPFAIL) as \"lost the spawn race\" and retries via readAndDial.\nconst LockHeldExitCode = 75\n\n// ErrLockHeld is returned from ListenAndServe when another proxy already\n// holds proxy.lock for the same rootDir. It is a normal \"lost the race\"\n// outcome, not a failure: callers spawned as children should map it to\n// LockHeldExitCode and exit cleanly.\nvar ErrLockHeld = errors.New(\"proxy lock held by another proxy on this rootDir\")\n\nconst (\n\tserverReadyTimeout     = 30 * time.Second\n\treadyDialTimeout       = 2 * time.Second\n\treadyInitialBackoff    = 50 * time.Millisecond\n\treadyMaxBackoff        = 1 * time.Second\n\tidleWatcherMinInterval = 1 * time.Second\n\tbackendStopTimeout     = 5 * time.Minute\n\ttcpKeepAlivePeriod     = 30 * time.Second\n)\n\nvar errIdleTimeout = errors.New(\"idle timeout reached\")\n\nfunc NewProxyServer(opts ProxyOpts) *proxyServer {\n\treturn &proxyServer{\n\t\trootDir:     opts.RootDir,\n\t\tport:        opts.Port,\n\t\tidleTimeout: opts.IdleTimeout,","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dbproxy/proxy/server.go#L62-L98","documentation":"ErrLockHeld is returned by ListenAndServe when another proxy process already holds proxy.lock for the same rootDir. It is an expected 'lost the race' outcome, not a failure: exactly one proxy per rootDir may serve. Child-spawned callers should map it to LockHeldExitCode (75) and exit cleanly.","triggerScenarios":"Starting a second proxy server (ListenAndServe) on a rootDir whose lock file is held by a live proxy; concurrent proxy instantiation tests where only one wins.","commonSituations":"Launching `bd` twice in the same workspace; a supervisor respawning the proxy while the old one still runs; leftover lock held by a healthy sibling process.","solutions":["Treat it as success: check errors.Is(err, proxy.ErrLockHeld), exit with LockHeldExitCode (75)","Use the already-running proxy instead of starting a new one","If truly stale (holder process is dead), clean up the stale proxy process/lock and retry"],"exampleFix":"// before\nif err := srv.ListenAndServe(ctx); err != nil { os.Exit(1) }\n// after\nif err := srv.ListenAndServe(ctx); err != nil {\n    if errors.Is(err, proxy.ErrLockHeld) {\n        os.Exit(proxy.LockHeldExitCode) // 75, clean exit\n    }\n    os.Exit(1)\n}","handlingStrategy":"try-catch","validationCode":"// no pre-check lock API; probe by attempting to dial the existing proxy first\nif _, err := proxy.DialExisting(ctx, root); err == nil {\n    return nil // a proxy is already serving this root\n}","typeGuard":null,"tryCatchPattern":"if err := srv.ListenAndServe(ctx); err != nil {\n    if errors.Is(err, proxy.ErrLockHeld) {\n        os.Exit(proxy.LockHeldExitCode) // clean child exit\n    }\n    return err\n}","preventionTips":["Always special-case ErrLockHeld as a benign outcome in proxy-spawning code","Use LockHeldExitCode (75) so supervisors can distinguish it from real failures","Avoid spawning a second proxy when a healthy one already responds on the socket"],"tags":["go","file-lock","concurrency"],"backgroundTag":"lock-already-held","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}