JuliusBrussee/caveman · error
open arm
Error message
open arm %q: %w
What it means
SwitchExperimentArm wraps the error from the INSERT that opens the new arm row after the previous arm was closed. This library throws it when starting the replacement arm fails, which can leave the experiment momentarily with no open arm (the close already succeeded).
Solutions
- Read the wrapped cause with errors.Unwrap to identify the SQLite failure.
- Retry the switch after resolving lock contention (busy_timeout).
- Verify experiment_arms schema (experiment_id, arm, started_at, ended_at).
- If the experiment is left armless, call SwitchExperimentArm again or StopExperiment to finalize cleanly.
Example fix
// before
if _, err := s.db.Exec(`INSERT INTO experiment_arms ...`, id, arm, now); err != nil {
return fmt.Errorf("open arm %q: %w", arm, err)
}
// after
if _, err := s.db.Exec(`INSERT INTO experiment_arms ...`, id, arm, now); err != nil {
return fmt.Errorf("open arm %q: %w", arm, err)
}
// caller: retry on busy
if errors.Is(err, sqlite.ErrBusy) { return store.SwitchExperimentArm(label, arm) } Defensive patterns
Strategy: retry
Try / catch
exp, err := store.SwitchExperimentArm(label, arm)
if err != nil {
// verify experiment state via LoadExperiment before retrying
return fmt.Errorf("switch arm: %w", err)
} Prevention
- Retry on SQLITE_BUSY with backoff
- Keep experiment_arms schema up to date
- Close other writers before switching
When it happens
Trigger: Calling Store.SwitchExperimentArm(label, arm) when 'INSERT INTO experiment_arms (experiment_id, arm, started_at)' fails: constraint violation, locked database, disk full.
Common situations: SQLITE_BUSY under concurrent writers; corrupted schema; disk quota exceeded mid-run.
Related errors
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/5b18595e712d1b59.
Report an issue: GitHub.
Appendix: source
Thrown at proxy/internal/store/learn_experiment.go:139
if err != nil {
return Experiment{}, err
}
if experiment.StoppedAt != "" {
return Experiment{}, fmt.Errorf("experiment %q is stopped", experiment.Label)
}
for _, existing := range experiment.Arms {
if existing.EndedAt == "" && existing.Arm == arm {
return experiment, nil
}
}
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(`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)View on GitHub (pinned to 3ee70a1026)