gastownhall/beads · error

start control listener: %w

Error message

start control listener: %w

What it means

Wraps a failure from startControl(p.rootDir, ...), which brings up the Unix-domain-socket/TCP control listener used for identity handshake on the proxy. If the control listener cannot be created or bound, ListenAndServe aborts with this error.

Source

Thrown at internal/storage/dbproxy/proxy/server.go:254

	}

	if _, err := identity.WriteSecret(p.rootDir); err != nil {
		return fmt.Errorf("write proxy secret: %w", err)
	}

	var identMu sync.RWMutex
	identReply := identity.IdentReply{
		Schema:   pidfile.SchemaV2,
		Role:     pidfile.KindProxy,
		DataPort: dataPort.Port,
	}
	control, err := startControl(p.rootDir, func() identity.IdentReply {
		identMu.RLock()
		defer identMu.RUnlock()
		return identReply
	})
	if err != nil {
		return fmt.Errorf("start control listener: %w", err)
	}
	defer func() { _ = control.Close() }()

	p.stats.IncBackendStart()
	if err := p.server.Start(ctx); err != nil {
		// Start failed with no backend left running (Start cleans up its own
		// failure), so there is no teardown to move off the lock; classifying
		// the epoch-watcher cancellation just keeps the child's exit reason
		// precise for the spawning parent.
		if changed, cerr := stopEpochChanged(p.rootDir, p.stopEpoch); cerr == nil && changed {
			return fmt.Errorf("%w for %s: stop epoch advanced during backend start (%v)", errStartInterrupted, p.rootDir, err)
		}
		return fmt.Errorf("start database server: %w", err)
	}

	if err := waitForServerReady(ctx, p.server, serverReadyTimeout); err != nil {
		if changed, cerr := stopEpochChanged(p.rootDir, p.stopEpoch); cerr == nil && changed {
			return abortInterruptedStart()

View on GitHub (pinned to 71377f2769)

Solutions

  1. Remove any stale control socket file in rootDir and retry
  2. Check write permissions on rootDir for the proxy process
  3. Shorten the socket path (use a shorter rootDir) if hitting the Unix socket path length limit
  4. Check for another live proxy holding the control endpoint

Example fix

// before
// stale socket left behind, startControl fails
// after
if fi, err := os.Lstat(sockPath); err == nil && fi.Mode()&os.ModeSocket != 0 {
    _ = os.Remove(sockPath) // clean stale socket before start
}
Defensive patterns

Strategy: fallback

Validate before calling

// Pre-clean stale control sockets under rootDir
entries, _ := filepath.Glob(filepath.Join(rootDir, "*.sock"))
for _, s := range entries {
    if fi, err := os.Lstat(s); err == nil && fi.Mode()&os.ModeSocket != 0 { os.Remove(s) }
}

Try / catch

if err := p.ListenAndServe(ctx); err != nil {
    if strings.Contains(err.Error(), "start control listener") {
        // clean stale socket and retry once
        os.RemoveAll(filepath.Join(rootDir, "control.sock"))
        err = p.ListenAndServe(ctx)
    }
    if err != nil { log.Fatal(err) }
}

Prevention

When it happens

Trigger: startControl fails because its socket path is unwritable, the socket file already exists, or its bind() fails due to permissions or address restrictions.

Common situations: Stale socket file left by a crashed proxy; rootDir permissions changed; socket path too long (>108 chars on Unix); another process bound the same control port/path.

Related errors


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