{"record":{"id":"a92baf3e79501e80","repo":"JuliusBrussee/caveman","slug":"close-open-arm-w","errorCode":null,"errorMessage":"close open arm: %w","messagePattern":"close open arm: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/internal/store/learn_experiment.go","lineNumber":135,"sourceCode":"\tif arm != experimentArmOn && arm != experimentArmOff {\n\t\treturn Experiment{}, fmt.Errorf(\"arm must be %q or %q\", experimentArmOn, experimentArmOff)\n\t}\n\texperiment, err := s.LoadExperiment(label)\n\tif err != nil {\n\t\treturn Experiment{}, err\n\t}\n\tif experiment.StoppedAt != \"\" {\n\t\treturn Experiment{}, fmt.Errorf(\"experiment %q is stopped\", experiment.Label)\n\t}\n\tfor _, existing := range experiment.Arms {\n\t\tif existing.EndedAt == \"\" && existing.Arm == arm {\n\t\t\treturn experiment, nil\n\t\t}\n\t}\n\tnow := learnOutcomeClock().UTC().Format(time.RFC3339)\n\tif _, err := s.db.Exec(`UPDATE experiment_arms SET ended_at = ?\n\t\tWHERE experiment_id = ? AND ended_at IS NULL`, now, experiment.id); err != nil {\n\t\treturn Experiment{}, fmt.Errorf(\"close open arm: %w\", err)\n\t}\n\tif _, err := s.db.Exec(`INSERT INTO experiment_arms (experiment_id, arm, started_at)\n\t\tVALUES (?, ?, ?)`, experiment.id, arm, now); err != nil {\n\t\treturn Experiment{}, fmt.Errorf(\"open arm %q: %w\", arm, err)\n\t}\n\treturn s.LoadExperiment(label)\n}\n\nfunc (s *Store) StopExperiment(label string) (Experiment, error) {\n\texperiment, err := s.LoadExperiment(label)\n\tif err != nil {\n\t\treturn Experiment{}, err\n\t}\n\tnow := learnOutcomeClock().UTC().Format(time.RFC3339)\n\tif _, err := s.db.Exec(`UPDATE experiment_arms SET ended_at = ?\n\t\tWHERE experiment_id = ? AND ended_at IS NULL`, now, experiment.id); err != nil {\n\t\treturn Experiment{}, fmt.Errorf(\"close open arm: %w\", err)\n\t}","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/proxy/internal/store/learn_experiment.go#L117-L153","documentation":"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.","triggerScenarios":"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.","commonSituations":"Concurrent writers hitting SQLITE_BUSY; schema drift after an upgrade; read-only database file.","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."],"exampleFix":"// before\nexp, err := store.SwitchExperimentArm(label, \"off\")\nif err != nil { return err }\n// after\nexp, err := store.SwitchExperimentArm(label, \"off\")\nif err != nil {\n    if errors.Is(err, sqlite.ErrBusy) { /* retry once */ }\n    return err\n}","handlingStrategy":"retry","validationCode":"var one int\n_ = s.db.QueryRow(`SELECT 1 FROM experiment_arms LIMIT 1`).Scan(&one) // table exists","typeGuard":null,"tryCatchPattern":"for i := 0; i < 3; i++ {\n    exp, err := store.SwitchExperimentArm(label, arm)\n    if err == nil { break }\n    if !isBusy(err) { return err }\n    time.Sleep(100*time.Millisecond << i)\n}","preventionTips":["Enable sqlite busy_timeout","Avoid long transactions during experiment mutations","Single-writer discipline for the store"],"tags":["go","database","sqlite","experiment"],"backgroundTag":"database-write-failed","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}