{"record":{"id":"f739b24a93eaf544","repo":"gastownhall/beads","slug":"check-proxy-stop-epoch-after-acquiring-s-w","errorCode":null,"errorMessage":"check proxy stop epoch after acquiring %s: %w","messagePattern":"check proxy stop epoch after acquiring (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/dbproxy/proxy/server.go","lineNumber":140,"sourceCode":"\treleaseLock := func() {\n\t\tif lockHeld {\n\t\t\tlockHeld = false\n\t\t\tlock.Unlock()\n\t\t}\n\t}\n\tdefer releaseLock()\n\tif err := clearSpawnMarkerAfterLock(p.rootDir); err != nil {\n\t\treturn fmt.Errorf(\"clear proxy spawn marker: %w\", err)\n\t}\n\n\t// Fast-abort: a concurrent `bd dolt stop` advances the stop epoch before\n\t// waiting (briefly) for proxy.lock, so an epoch that moved between the\n\t// spawning parent's read and this child taking the lock dooms this start.\n\t// Abort before opening any listener or booting the backend: the stopper\n\t// then observes a free lock within milliseconds instead of after a full\n\t// doomed boot-and-teardown cycle.\n\tif changed, err := stopEpochChanged(p.rootDir, p.stopEpoch); err != nil {\n\t\treturn fmt.Errorf(\"check proxy stop epoch after acquiring %s: %w\", LockFileName, err)\n\t} else if changed {\n\t\treturn fmt.Errorf(\"%w for %s: stop epoch advanced before startup\", errStartInterrupted, p.rootDir)\n\t}\n\n\tlogPath := filepath.Join(p.rootDir, LogFileName)\n\tf, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) // #nosec G304 -- logPath is derived from operator-supplied config, not untrusted request input\n\tif err != nil {\n\t\treturn fmt.Errorf(\"open proxy log %q: %w\", logPath, err)\n\t}\n\tp.logger = log.New(f, \"[proxy] \", log.LstdFlags|log.Lmicroseconds)\n\tdefer func() { _ = f.Close() }()\n\n\tctx, cancel := context.WithCancel(parentCtx)\n\tdefer cancel()\n\n\t// Install signal handlers BEFORE Listen. Without this, Go's default\n\t// SIGTERM action terminates the process during the startup window\n\t// (Listen, pidfile write, backend Start, readiness wait), bypassing all","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dbproxy/proxy/server.go#L122-L158","documentation":"After taking proxy.lock, the child re-checks the stop epoch to detect a concurrent `bd dolt stop` that advanced it while the child was waiting for the lock. If reading/comparing the epoch fails, startup aborts with this wrapped error; it must not boot a backend on unknown epoch state.","triggerScenarios":"ListenAndServe calls stopEpochChanged(p.rootDir, p.stopEpoch) right after locking, and the epoch read/compare returns an error (unreadable epoch file, I/O failure).","commonSituations":"Concurrent stop corrupting or removing the epoch file mid-read; permission issues on rootDir state files; disk I/O errors.","solutions":["Retry starting the proxy once the concurrent stop has completed","Fix permissions/availability of the epoch file in rootDir","Stop any concurrent `bd dolt stop` processes racing the start"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// ensure no stop is in flight before starting\nif epoch, err := readStopEpoch(rootDir); err != nil { return err } // pre-read sanity","typeGuard":null,"tryCatchPattern":"if err := p.ListenAndServe(ctx); err != nil {\n  if strings.Contains(err.Error(), \"stop epoch\") { /* stop finished; retry start */ }\n  return err\n}","preventionTips":["Don't run bd dolt stop concurrently with starts","Keep rootDir state files readable by the daemon user","Retry starts after concurrent stops complete"],"tags":["concurrency","stop-epoch","startup"],"backgroundTag":"stop-epoch-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}