gastownhall/beads · warning
server: ExternalDoltServer.Start: server already started
Error message
server: ExternalDoltServer.Start: server already started
What it means
ExternalDoltServer wraps an already-running Dolt server (no child process to spawn), so Start is just a guard that marks the wrapper as started using an atomic compare-and-swap. Calling Start twice without an intervening Stop is treated as a programming error and returns this sentinel instead of silently succeeding.
Source
Thrown at internal/storage/dbproxy/server/external_dolt_server.go:80
func (s *ExternalDoltServer) DSN(_ context.Context, database, user, password string) string {
dsn := util.DoltServerDSN{
User: user,
Password: password,
Database: database,
}
if s.socket != "" {
dsn.Socket = s.socket
} else {
dsn.Host = s.host
dsn.Port = s.port
}
return dsn.String()
}
func (s *ExternalDoltServer) Start(_ context.Context) error {
if !s.started.CompareAndSwap(false, true) {
return errors.New("server: ExternalDoltServer.Start: server already started")
}
return nil
}
func (s *ExternalDoltServer) Stop(_ context.Context) error {
s.started.Store(false)
return nil
}
func (s *ExternalDoltServer) Running(_ context.Context) bool {
return s.started.Load()
}
func (s *ExternalDoltServer) Dial(ctx context.Context) (net.Conn, error) {
network, addr := "tcp", net.JoinHostPort(s.host, strconv.Itoa(s.port))
if s.socket != "" {
network, addr = "unix", s.socket
}View on GitHub (pinned to 71377f2769)
Solutions
- Call Start once per instance; call Stop before any legitimate restart
- Make startup idempotent in your code: skip Start when the server is already running
- Serialize lifecycle management behind a mutex or Once so concurrent callers cannot double-Start
- If the double-start is expected in your design, treat this sentinel as a no-op success
Example fix
// before
if err := srv.Start(ctx); err != nil { return err } // panics on second call in retries
if err := srv.Start(ctx); err != nil { return err }
// after
var startOnce sync.Once
startOnce.Do(func() { startErr = srv.Start(ctx) })
if startErr != nil { return startErr } Defensive patterns
Strategy: type-guard
Validate before calling
var started atomic.Bool // track in the owner before calling Start
if !started.CompareAndSwap(false, true) {
return nil // already started; skip
} Type guard
func isAlreadyStarted(err error) bool {
return err != nil && strings.Contains(err.Error(), "server already started")
} Try / catch
if err := srv.Start(ctx); err != nil {
if isAlreadyStarted(err) {
return nil // idempotent: treat as success
}
return err
} Prevention
- Pair every Start with exactly one Stop in the same owner (defer Stop)
- Guard startup with sync.Once or an atomic flag in your wiring
- Never call Start from multiple code paths (init + request handler) on the same instance
When it happens
Trigger: Calling ExternalDoltServer.Start a second time on the same instance after a previous Start succeeded and Stop has not been called (or is still in flight), because started.CompareAndSwap(false, true) fails on the re-entry.
Common situations: Retry loops that re-call Start after a partial startup; wiring Start into both an init function and a request handler; multiple goroutines starting the same server concurrently; confusion with DoltServer, whose Start semantics differ.
Related errors
- ErrLockHeld
- errIdleTimeout
- server: NewDoltServer: failed to determine absolute path of
- server: NewDoltServer: failed to determine absolute path of
- server: NewDoltServer: failed to determine absolute path of
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/a9b08b7cd28e9ac8.
Report an issue: GitHub.