gastownhall/beads · error

acquire

Error message

acquire %s: %w

What it means

ListenAndServe first acquires proxy.lock under rootDir; failure to create or lock the file is wrapped as 'acquire %s: %w'. The lock serializes the whole proxy lifetime, so without it the proxy must not start. A doomed start may release the lock early, but only the main goroutine touches it.

Solutions

  1. Stop the other proxy process using the same rootDir (bd dolt stop or kill it)
  2. Remove a stale lock only after confirming no live holder
  3. Fix rootDir permissions so the lock file can be created/locked

Example fix

// before: second daemon against same root
bd serve --root /data   // holds proxy.lock
bd serve --root /data   // acquire error
// after
bd dolt stop --root /data && bd serve --root /data
Defensive patterns

Strategy: fallback

Validate before calling

// before starting, check for a live holder
if f, err := os.Open(filepath.Join(rootDir, "proxy.lock")); err == nil {
  defer f.Close()
  if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
    return fmt.Errorf("proxy already running for %s", rootDir)
  }
}

Try / catch

if err := p.ListenAndServe(ctx); err != nil {
  if strings.Contains(err.Error(), "acquire") { /* another proxy holds the lock */ }
  return err
}

Prevention

When it happens

Trigger: Calling proxyServer.ListenAndServe when proxy.lock cannot be created/locked in rootDir — another proxy holds the lock, or the lock file can't be opened/created.

Common situations: Two `bd` daemons started against the same rootDir; leftover lock after a crashed start (usually auto-released by flock semantics); read-only or permission-restricted rootDir; NFS mounts without flock support.

Related errors


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

Appendix: source

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

		port:        opts.Port,
		idleTimeout: opts.IdleTimeout,
		server:      opts.Server,
		stats:       opts.Stats,
		stopEpoch:   opts.StopEpoch,
	}
}

func (p *proxyServer) tracef(format string, args ...any) {
	p.logger.Printf(format, args...)
}

func (p *proxyServer) ListenAndServe(parentCtx context.Context) error {
	lock, err := util.TryLock(filepath.Join(p.rootDir, LockFileName))
	if err != nil {
		if lockfile.IsLocked(err) {
			return ErrLockHeld
		}
		return fmt.Errorf("acquire %s: %w", LockFileName, err)
	}
	// proxy.lock is held for the proxy's whole lifetime, but a doomed start
	// must be able to release it early (before its backend teardown) without
	// the deferred release double-unlocking. Only the main goroutine touches
	// this.
	lockHeld := true
	releaseLock := func() {
		if lockHeld {
			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

View on GitHub (pinned to 71377f2769)