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

  1. Call Start once per instance; call Stop before any legitimate restart
  2. Make startup idempotent in your code: skip Start when the server is already running
  3. Serialize lifecycle management behind a mutex or Once so concurrent callers cannot double-Start
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/a9b08b7cd28e9ac8. Report an issue: GitHub.