{"record":{"id":"c98782418f93b9d3","repo":"slackhq/nebula","slug":"erralreadystarted","errorCode":"ErrAlreadyStarted","errorMessage":"nebula is already started","messagePattern":"nebula is already started","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"control.go","lineNumber":28,"sourceCode":"\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/slackhq/nebula/cert\"\n\t\"github.com/slackhq/nebula/header\"\n\t\"github.com/slackhq/nebula/overlay\"\n)\n\ntype RunState int\n\nconst (\n\tStateUnknown RunState = iota\n\tStateReady\n\tStateStarted\n\tStateStopping\n\tStateStopped\n)\n\nvar ErrAlreadyStarted = errors.New(\"nebula is already started\")\nvar ErrAlreadyStopped = errors.New(\"nebula cannot be restarted\")\nvar ErrUnknownState = errors.New(\"nebula state is invalid\")\n\n// Every interaction here needs to take extra care to copy memory and not return or use arguments \"as is\" when touching\n// core. This means copying IP objects, slices, de-referencing pointers and taking the actual value, etc\n\ntype controlEach func(h *HostInfo)\n\ntype controlHostLister interface {\n\tQueryVpnAddr(vpnAddr netip.Addr) *HostInfo\n\tForEachIndex(each controlEach)\n\tForEachVpnAddr(each controlEach)\n\tGetPreferredRanges() []netip.Prefix\n}\n\ntype Control struct {\n\tstateLock sync.Mutex\n\tstate     RunState","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/control.go#L10-L46","documentation":"ErrAlreadyStarted in control.go means Control.Start() was called while the instance is already in StateStarted. The state machine in Start only accepts StateReady; StateStarted returns this error, StateStopped/Stopping return ErrAlreadyStopped, anything else returns ErrUnknownState.","triggerScenarios":"Calling c.Start() twice without an intervening Stop (control.go:83); asserted in TestControl_StartStopLifecycle and control_lifecycle_test.go:277.","commonSituations":"Supervisor/systemd double-start attempts, retry logic that re-invokes Start on ambiguous failure, tests or hot-reload paths starting an already-running instance.","solutions":["Check c.State() (or track lifecycle yourself) before calling Start.","Treat ErrAlreadyStarted as a no-op success: use errors.Is(err, ErrAlreadyStarted) to skip instead of failing.","Restructure startup to a single idempotent entry point guarded by sync.Once or a mutex.","If a restart is intended, call Stop() first and wait for StateStopped."],"exampleFix":"// before\nc.Start()\n...\nc.Start() // panics flow with ErrAlreadyStarted\n\n// after\nif err := c.Start(); err != nil && !errors.Is(err, ErrAlreadyStarted) {\n    return err\n}","handlingStrategy":"try-catch","validationCode":"if c.State() == control.StateStarted {\n    // already running; skip Start\n    return nil\n}","typeGuard":"func canStart(c *control.Control) bool {\n    return c.State() == control.StateReady\n}","tryCatchPattern":"if err := c.Start(); err != nil {\n    if errors.Is(err, control.ErrAlreadyStarted) {\n        return nil // idempotent\n    }\n    return err\n}","preventionTips":["Route all Start calls through one idempotent supervisor function.","Use sync.Once or a state flag to deduplicate start attempts.","Check State() before Start in health-check/retry loops.","Never treat ErrAlreadyStarted as fatal in double-start-prone environments like systemd."],"tags":["lifecycle","state-machine","go"],"backgroundTag":"already-started","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}