JuliusBrussee/caveman · error

stop experiment

Error message

stop experiment: %w

What it means

StopExperiment wraps the error from the UPDATE that sets experiments.stopped_at. This library throws it when finalizing the experiment row fails; note the arms were already closed by the preceding UPDATE, so a failure here leaves arms closed but stopped_at unset.

Solutions

  1. Unwrap the error to see the underlying SQLite cause.
  2. Retry after resolving lock contention (busy_timeout).
  3. Confirm experiments table exists with a nullable stopped_at column.
  4. If stuck with closed arms but no stopped_at, re-run StopExperiment — it is idempotent for the arm-closing step.

Example fix

// before
if _, err := s.db.Exec(`UPDATE experiments SET stopped_at = ? WHERE id = ?`, now, experiment.id); err != nil {
    return Experiment{}, fmt.Errorf("stop experiment: %w", err)
}
// after
// same code; caller-side retry on busy:
if _, err := store.StopExperiment(label); err != nil {
    if isBusy(err) { time.Sleep(time.Second); _, err = store.StopExperiment(label) }
    return err
}
Defensive patterns

Strategy: retry

Validate before calling

var stopped string
_ = s.db.QueryRow(`SELECT COALESCE(stopped_at,'') FROM experiments WHERE label = ?`, label).Scan(&stopped) // existence + state check

Try / catch

_, err := store.StopExperiment(label)
if err != nil {
    if isBusy(err) { /* retry */ }
    return fmt.Errorf("stop: %w", err)
}

Prevention

When it happens

Trigger: Calling Store.StopExperiment(label) when 'UPDATE experiments SET stopped_at = ? WHERE id = ?' fails: database locked, missing experiments table, disk error.

Common situations: Concurrent write contention on SQLite; schema drift; the DB file becoming read-only mid-operation.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/a58facaad22c1ff8. Report an issue: GitHub.

Appendix: source

Thrown at proxy/internal/store/learn_experiment.go:155

	if _, err := s.db.Exec(`INSERT INTO experiment_arms (experiment_id, arm, started_at)
		VALUES (?, ?, ?)`, experiment.id, arm, now); err != nil {
		return Experiment{}, fmt.Errorf("open arm %q: %w", arm, err)
	}
	return s.LoadExperiment(label)
}

func (s *Store) StopExperiment(label string) (Experiment, error) {
	experiment, err := s.LoadExperiment(label)
	if err != nil {
		return Experiment{}, err
	}
	now := learnOutcomeClock().UTC().Format(time.RFC3339)
	if _, err := s.db.Exec(`UPDATE experiment_arms SET ended_at = ?
		WHERE experiment_id = ? AND ended_at IS NULL`, now, experiment.id); err != nil {
		return Experiment{}, fmt.Errorf("close open arm: %w", err)
	}
	if _, err := s.db.Exec(`UPDATE experiments SET stopped_at = ? WHERE id = ?`, now, experiment.id); err != nil {
		return Experiment{}, fmt.Errorf("stop experiment: %w", err)
	}
	return s.LoadExperiment(label)
}

func (s *Store) LoadExperiment(label string) (Experiment, error) {
	label = strings.TrimSpace(label)
	row := s.db.QueryRow(`SELECT id, label, COALESCE(sink_id,''), COALESCE(fix_kind,''),
		COALESCE(note,''), created_at, COALESCE(stopped_at,'') FROM experiments WHERE label = ?`, label)
	var experiment Experiment
	if err := row.Scan(&experiment.id, &experiment.Label, &experiment.SinkID, &experiment.FixKind,
		&experiment.Note, &experiment.CreatedAt, &experiment.StoppedAt); err != nil {
		if err == sql.ErrNoRows {
			return Experiment{}, fmt.Errorf("no experiment named %q", label)
		}
		return Experiment{}, err
	}
	rows, err := s.db.Query(`SELECT arm, started_at, COALESCE(ended_at,'')
		FROM experiment_arms WHERE experiment_id = ? ORDER BY started_at, id`, experiment.id)

View on GitHub (pinned to 3ee70a1026)