{"record":{"id":"33fdee6461f65b9c","repo":"JuliusBrussee/caveman","slug":"experiment-q-is-stopped","errorCode":null,"errorMessage":"experiment %q is stopped","messagePattern":"experiment %q is stopped","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/internal/store/learn_experiment.go","lineNumber":125,"sourceCode":"\t\treturn Experiment{}, fmt.Errorf(\"open first arm: %w\", err)\n\t}\n\treturn s.LoadExperiment(label)\n}\n\n// SwitchExperimentArm closes the open arm and opens the other one. Switching to\n// the arm already running is a no-op rather than an error: it keeps a repeated\n// call from fragmenting the interval history.\nfunc (s *Store) SwitchExperimentArm(label, arm string) (Experiment, error) {\n\tarm = strings.ToLower(strings.TrimSpace(arm))\n\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","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/proxy/internal/store/learn_experiment.go#L107-L143","documentation":"SwitchExperimentArm refuses to change arms on an experiment whose stopped_at timestamp is already set. Once an experiment is stopped its interval history is final, so any further arm switches are rejected.","triggerScenarios":"Calling Store.SwitchExperimentArm on a label that was previously passed to StopExperiment (experiments.stopped_at non-empty).","commonSituations":"A script that stops experiments at the end of a run then attempts to re-enable an arm; reusing a familiar experiment label after it was finalized; a cron job racing with a manual stop.","solutions":["Create a new experiment with StartExperiment instead of switching arms on the stopped one.","Check the experiment's StoppedAt field (via LoadExperiment/ListExperiments) before switching.","Pick a fresh unique label for the new experiment run.","If stopping was a mistake, stop using the record — this API has no un-stop; recreate the experiment."],"exampleFix":"// before\nexp, err := store.SwitchExperimentArm(label, \"off\")\n// after\nexp, err := store.LoadExperiment(label)\nif err == nil && exp.StoppedAt != \"\" {\n    return store.StartExperiment(label + \"-2\")\n}\nreturn store.SwitchExperimentArm(label, \"off\")","handlingStrategy":"validation","validationCode":"exp, err := store.LoadExperiment(label)\nif err == nil && exp.StoppedAt != \"\" { // stopped; don't switch\n    return ErrAlreadyStopped\n}","typeGuard":null,"tryCatchPattern":"_, err := store.SwitchExperimentArm(label, arm)\nif err != nil && strings.Contains(err.Error(), \"is stopped\") {\n    // start a new experiment instead\n}","preventionTips":["Check StoppedAt via LoadExperiment before switching","Generate fresh labels per run instead of reusing","Centralize experiment lifecycle in one place"],"tags":["go","state","experiment","validation"],"backgroundTag":"invalid-state-transition","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"}