gastownhall/beads · error

proxy.ForceStopUnverified: timeout must be positive, got %s

Error message

proxy.ForceStopUnverified: timeout must be positive, got %s

What it means

ForceStopUnverified validates its optional Timeout option and rejects non-positive values because a non-positive timeout makes the force-stop wait loop meaningless. The library throws this synchronously at the start of the call before any destructive action is taken.

Source

Thrown at internal/storage/dbproxy/proxy/force_stop.go:65

// inspecting, signaling, and quarantining the unchanged record. When the lock
// is held (the usual pre-upgrade-proxy case), it first inspects and signals
// the live PID, waits for the lock to become free, then quarantines only if
// the record is unchanged. Both flows accept an already-gone recorded
// process. An unverified live PID is never signaled unless its executable
// basename is exactly bd or dolt (with an optional .exe suffix) AND its
// command line scopes it to this workspace; where the platform cannot
// establish that scope, force-stop refuses rather than guessing.
func ForceStopUnverified(rootDir string, opts ...ForceStopOptions) (ForceStopReport, error) {
	report := ForceStopReport{RecordPath: pidfile.Path(rootDir, PIDFileName)}
	if len(opts) > 1 {
		return report, errors.New("proxy.ForceStopUnverified: at most one options value is allowed")
	}
	timeout := shutdownConfirmDeadline
	if len(opts) == 1 && opts[0].Timeout != 0 {
		timeout = opts[0].Timeout
	}
	if timeout <= 0 {
		return report, fmt.Errorf("proxy.ForceStopUnverified: timeout must be positive, got %s", timeout)
	}
	if err := advanceStopEpoch(rootDir); err != nil {
		return report, fmt.Errorf("proxy.ForceStopUnverified: publish stop epoch: %w", err)
	}

	proxyErr := forceStopRecord(rootDir, LockFileName, PIDFileName, pidfile.KindProxy, timeout, &report)

	backendReport := ForceStopReport{RecordPath: pidfile.Path(rootDir, server.PIDFileName)}
	backendErr := forceStopRecord(
		rootDir,
		server.LockFileName,
		server.PIDFileName,
		pidfile.KindDoltBackend,
		timeout,
		&backendReport,
	)
	if backendReport.RecordFound || backendErr != nil {
		report.Backend = &backendReport

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pass a positive timeout, e.g. proxy.WithTimeout(30*time.Second)
  2. Omit the timeout option entirely to use the built-in shutdownConfirmDeadline default
  3. Fix the caller so a zero/negative configured timeout falls back to a positive default before calling ForceStopUnverified

Example fix

// before
report, err := proxy.ForceStopUnverified(rootDir, proxy.WithTimeout(cfg.StopTimeout)) // cfg.StopTimeout == 0
// after
timeout := cfg.StopTimeout
if timeout <= 0 {
    timeout = 30 * time.Second
}
report, err := proxy.ForceStopUnverified(rootDir, proxy.WithTimeout(timeout))
Defensive patterns

Strategy: validation

Validate before calling

func safeTimeout(d time.Duration) time.Duration {
    if d <= 0 {
        return 30 * time.Second
    }
    return d
}
// usage: proxy.WithTimeout(safeTimeout(cfg.StopTimeout))

Type guard

func validTimeout(d time.Duration) bool { return d > 0 }

Try / catch

report, err := proxy.ForceStopUnverified(rootDir, opts...)
if err != nil && strings.Contains(err.Error(), "timeout must be positive") {
    report, err = proxy.ForceStopUnverified(rootDir) // use library default
}

Prevention

When it happens

Trigger: Calling proxy.ForceStopUnverified(rootDir, proxy.WithTimeout(0)) or WithTimeout(negative duration); note that a single option with Timeout==0 is treated as 'not set' and defaults to shutdownConfirmDeadline, but any explicitly non-positive timeout other than that zero-in-one-option case triggers this error (e.g. WithTimeout(-time.Second)).

Common situations: Test code or callers computing a timeout from a config value that is unset/zero/negative, passing time.Duration(0) thinking it means 'infinite' or 'default'.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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