{"record":{"id":"00f0a49ce0793438","repo":"gastownhall/beads","slug":"clear-proxy-spawn-marker-w","errorCode":null,"errorMessage":"clear proxy spawn marker: %w","messagePattern":"clear proxy spawn marker: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/dbproxy/proxy/server.go","lineNumber":130,"sourceCode":"\t\tif lockfile.IsLocked(err) {\n\t\t\treturn ErrLockHeld\n\t\t}\n\t\treturn fmt.Errorf(\"acquire %s: %w\", LockFileName, err)\n\t}\n\t// proxy.lock is held for the proxy's whole lifetime, but a doomed start\n\t// must be able to release it early (before its backend teardown) without\n\t// the deferred release double-unlocking. Only the main goroutine touches\n\t// this.\n\tlockHeld := true\n\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)","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dbproxy/proxy/server.go#L112-L148","documentation":"After acquiring proxy.lock, ListenAndServe clears the proxy spawn marker written by the spawning parent. If clearSpawnMarkerAfterLock fails, startup aborts with this wrapped error, since a leftover marker could make a future stop/wait misbehave.","triggerScenarios":"Calling ListenAndServe when clearSpawnMarkerAfterLock(p.rootDir) fails — marker file exists but cannot be removed (permissions, race with another remover, filesystem error).","commonSituations":"Read-only or full filesystem; rootDir ownership changed; two children spawned concurrently both racing to clear the marker.","solutions":["Inspect the wrapped %w cause and fix filesystem permissions on the marker file","Manually remove the stale spawn marker and restart the proxy","Ensure only one spawner/child pair operates per rootDir"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"marker := filepath.Join(rootDir, markerFile)\nif _, err := os.Stat(marker); err == nil {\n  if err := os.Remove(marker); err != nil { return err } // fail fast, pre-check\n}","typeGuard":null,"tryCatchPattern":"if err := p.ListenAndServe(ctx); err != nil {\n  if strings.Contains(err.Error(), \"spawn marker\") { /* check perms, retry */ }\n  return err\n}","preventionTips":["Ensure the daemon user owns rootDir state files","Avoid spawning two children for the same rootDir","Manually clear stale markers after crashed starts"],"tags":["filesystem","startup","spawn-marker"],"backgroundTag":"spawn-marker-cleanup-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}