gastownhall/beads · info
ErrLockHeld
ErrLockHeld
Error message
proxy lock held by another proxy on this rootDir
What it means
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.
Source
Thrown at internal/storage/dbproxy/proxy/server.go:80
conns errgroup.Group
}
const (
PIDFileName = "proxy.pid"
LogFileName = "proxy.log"
LockFileName = "proxy.lock"
)
// LockHeldExitCode is the exit code a child proxy should use when
// ListenAndServe returns ErrLockHeld. The spawning parent treats this
// (EX_TEMPFAIL) as "lost the spawn race" and retries via readAndDial.
const LockHeldExitCode = 75
// ErrLockHeld is returned from ListenAndServe when another proxy already
// holds proxy.lock for the same rootDir. It is a normal "lost the race"
// outcome, not a failure: callers spawned as children should map it to
// LockHeldExitCode and exit cleanly.
var ErrLockHeld = errors.New("proxy lock held by another proxy on this rootDir")
const (
serverReadyTimeout = 30 * time.Second
readyDialTimeout = 2 * time.Second
readyInitialBackoff = 50 * time.Millisecond
readyMaxBackoff = 1 * time.Second
idleWatcherMinInterval = 1 * time.Second
backendStopTimeout = 5 * time.Minute
tcpKeepAlivePeriod = 30 * time.Second
)
var errIdleTimeout = errors.New("idle timeout reached")
func NewProxyServer(opts ProxyOpts) *proxyServer {
return &proxyServer{
rootDir: opts.RootDir,
port: opts.Port,
idleTimeout: opts.IdleTimeout,View on GitHub (pinned to 71377f2769)
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
Example fix
// before
if err := srv.ListenAndServe(ctx); err != nil { os.Exit(1) }
// after
if err := srv.ListenAndServe(ctx); err != nil {
if errors.Is(err, proxy.ErrLockHeld) {
os.Exit(proxy.LockHeldExitCode) // 75, clean exit
}
os.Exit(1)
} Defensive patterns
Strategy: try-catch
Validate before calling
// no pre-check lock API; probe by attempting to dial the existing proxy first
if _, err := proxy.DialExisting(ctx, root); err == nil {
return nil // a proxy is already serving this root
} Try / catch
if err := srv.ListenAndServe(ctx); err != nil {
if errors.Is(err, proxy.ErrLockHeld) {
os.Exit(proxy.LockHeldExitCode) // clean child exit
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- acquiring lock (blocking): %w
- acquiring lock (non-blocking): %w
- lock busy: held by another process
- lock already held by another process
- server: ExternalDoltServer.Start: server already started
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/8cd41c8bd7330d29.
Report an issue: GitHub.