gastownhall/beads · warning · errStartInterrupted
%w for %s: stop epoch advanced before startup
Error message
%w for %s: stop epoch advanced before startup
What it means
If the stop epoch advanced between the parent's read and this child acquiring proxy.lock, the start is doomed — a stopper is already waiting. The proxy returns this error (wrapping errStartInterrupted) before opening any listener or booting the backend so the stopper observes a free lock within milliseconds instead of after a full boot-and-teardown cycle.
Source
Thrown at internal/storage/dbproxy/proxy/server.go:142
lockHeld = false
lock.Unlock()
}
}
defer releaseLock()
if err := clearSpawnMarkerAfterLock(p.rootDir); err != nil {
return fmt.Errorf("clear proxy spawn marker: %w", err)
}
// Fast-abort: a concurrent `bd dolt stop` advances the stop epoch before
// waiting (briefly) for proxy.lock, so an epoch that moved between the
// spawning parent's read and this child taking the lock dooms this start.
// Abort before opening any listener or booting the backend: the stopper
// then observes a free lock within milliseconds instead of after a full
// doomed boot-and-teardown cycle.
if changed, err := stopEpochChanged(p.rootDir, p.stopEpoch); err != nil {
return fmt.Errorf("check proxy stop epoch after acquiring %s: %w", LockFileName, err)
} else if changed {
return fmt.Errorf("%w for %s: stop epoch advanced before startup", errStartInterrupted, p.rootDir)
}
logPath := filepath.Join(p.rootDir, LogFileName)
f, 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
if err != nil {
return fmt.Errorf("open proxy log %q: %w", logPath, err)
}
p.logger = log.New(f, "[proxy] ", log.LstdFlags|log.Lmicroseconds)
defer func() { _ = f.Close() }()
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
// Install signal handlers BEFORE Listen. Without this, Go's default
// SIGTERM action terminates the process during the startup window
// (Listen, pidfile write, backend Start, readiness wait), bypassing all
// deferred cleanup including RemoveDatabaseProxyPidFile.
sigCh := make(chan os.Signal, 1)View on GitHub (pinned to 71377f2769)
Solutions
- Re-run the start command now that the stop has finished
- Coordinate start/stop in scripts (wait for stop to complete before starting)
- If restart managers are involved, disable auto-restart during intentional stops
Defensive patterns
Strategy: retry
Validate before calling
// check epoch before spawning
epoch, err := readStopEpoch(rootDir)
if err != nil { return err }
_ = epoch // pass to child; mismatch means abort Try / catch
err := p.ListenAndServe(ctx)
if errors.Is(err, errStartInterrupted) { /* expected during concurrent stop; restart if desired */ } Prevention
- Serialize start/stop in scripts and supervisors
- Disable auto-restart during intentional stops
- Treat errStartInterrupted as normal during stop windows
When it happens
Trigger: ListenAndServe detects stopEpochChanged == true immediately after acquiring proxy.lock — i.e. `bd dolt stop` ran concurrently with the spawn.
Common situations: Race between a script that starts the daemon and one that stops it; test suites running start/stop concurrently; operator stopping a service right as an auto-restart spawns it.
Related errors
- %w for %s: stop epoch advanced during startup
- target git directory identity changed
- check proxy stop epoch after acquiring %s: %w
- failed to unclaim issue %s: no matching row
- lock busy: held by another process
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/d32415558299cb71.
Report an issue: GitHub.