gastownhall/beads · error

discover spawned proxy: %w

Error message

discover spawned proxy: %w

What it means

After successfully forking the child proxy, the parent polls by re-reading the pidfile (readAndDial) until the child records its endpoint. If that post-spawn discovery read fails with an I/O error, the parent cannot confirm the child started, so it aborts and wraps the read error. This differs from the timeout path: the record was there but unreadable during the confirmation window.

Source

Thrown at internal/storage/dbproxy/proxy/endpoint.go:361

			_ = child.handle.Close()
		}
	}()

	hard := time.NewTimer(spawnReadyHardTimeout)
	defer hard.Stop()
	poll := time.NewTicker(openPollInterval)
	defer poll.Stop()

	for {
		discovered := readAndDial(rootDir)
		if discovered.status == adoptionAdopted {
			if err := sweepOldQuarantines(rootDir, time.Now()); err != nil {
				log.Printf("dbproxy: could not sweep old quarantined records in %s: %v", rootDir, err)
			}
			return discovered.endpoint, nil
		}
		if discovered.status == adoptionIOErr {
			return Endpoint{}, fmt.Errorf("discover spawned proxy: %w", discovered.err)
		}
		select {
		case childErr := <-child.done:
			if interrupted, ierr := stopEpochChanged(rootDir, stopEpoch); ierr != nil {
				return Endpoint{}, ierr
			} else if interrupted {
				return Endpoint{}, fmt.Errorf("%w for %s", errStartInterrupted, rootDir)
			}
			if childErr == nil {
				childErr = errors.New("child exited without reporting an error")
			}
			// A LockHeldExitCode exit is a lost spawn race, not a listen
			// failure; any other exit gets the child's log path so the real
			// error (listen, backend start, ...) is findable.
			var exitErr *exec.ExitError
			if errors.As(childErr, &exitErr) && exitErr.ExitCode() == LockHeldExitCode {
				return Endpoint{}, fmt.Errorf(
					"proxy child lost the proxy.lock spawn race for %s: %w",

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the open once — transient read races usually clear on the next poll cycle
  2. Check whether the child actually started and its log (opts.LogFilePath) for crash details
  3. Fix permissions on the workspace pidfile path for the running user
  4. If persistent, move the workspace to a local (non-NFS) filesystem

Example fix

// before
ep, err := proxy.GetCreateDatabaseProxyServerEndpoint(root, opts) // fails once on NFS hiccup
// after
ep, err := proxy.GetCreateDatabaseProxyServerEndpoint(root, opts)
if err != nil && errors.Is(err, io.ErrPermission) || isTransientIO(err) {
    time.Sleep(time.Second)
    ep, err = proxy.GetCreateDatabaseProxyServerEndpoint(root, opts)
}
Defensive patterns

Strategy: retry

Try / catch

ep, err := proxy.GetCreateDatabaseProxyServerEndpoint(root, opts)
if err != nil && strings.HasPrefix(err.Error(), "discover spawned proxy:") {
    time.Sleep(1 * time.Second)             // transient pidfile read race
    ep, err = proxy.GetCreateDatabaseProxyServerEndpoint(root, opts)
}

Prevention

When it happens

Trigger: During the wait loop inside spawnAndHandoff, discovered.status == adoptionIOErr — the newly spawned child's pidfile cannot be read (permission flip, partial write being read concurrently, FS error) while waiting for the endpoint to appear.

Common situations: Slow network filesystems where the record appears but reads fail transiently; security software interfering with newly created files; child crashing mid-write leaving a torn record that then reads as an IO error.

Related errors


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