JuliusBrussee/caveman · error
close open arm
Error message
close open arm: %w
What it means
SwitchExperimentArm wraps the error from the UPDATE that sets ended_at on the currently open experiment_arms row. This library throws it when closing the running arm fails in SQLite, before any new arm is opened.
Solutions
- Inspect the wrapped error via errors.Unwrap for the underlying SQLite cause.
- Enable busy_timeout / retry on SQLITE_BUSY and re-run the switch.
- Verify the experiment_arms schema includes ended_at (nullable).
- Ensure single-writer access or serialize experiment mutations.
Example fix
// before
exp, err := store.SwitchExperimentArm(label, "off")
if err != nil { return err }
// after
exp, err := store.SwitchExperimentArm(label, "off")
if err != nil {
if errors.Is(err, sqlite.ErrBusy) { /* retry once */ }
return err
} Defensive patterns
Strategy: retry
Validate before calling
var one int _ = s.db.QueryRow(`SELECT 1 FROM experiment_arms LIMIT 1`).Scan(&one) // table exists
Try / catch
for i := 0; i < 3; i++ {
exp, err := store.SwitchExperimentArm(label, arm)
if err == nil { break }
if !isBusy(err) { return err }
time.Sleep(100*time.Millisecond << i)
} Prevention
- Enable sqlite busy_timeout
- Avoid long transactions during experiment mutations
- Single-writer discipline for the store
When it happens
Trigger: Calling Store.SwitchExperimentArm(label, arm) when 'UPDATE experiment_arms SET ended_at = ? WHERE experiment_id = ? AND ended_at IS NULL' fails: locked DB, missing table/column, disk I/O error.
Common situations: Concurrent writers hitting SQLITE_BUSY; schema drift after an upgrade; read-only database file.
Related errors
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/a92baf3e79501e80.
Report an issue: GitHub.
Appendix: source
Thrown at proxy/internal/store/learn_experiment.go:135
if arm != experimentArmOn && arm != experimentArmOff {
return Experiment{}, fmt.Errorf("arm must be %q or %q", experimentArmOn, experimentArmOff)
}
experiment, err := s.LoadExperiment(label)
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)
}View on GitHub (pinned to 3ee70a1026)