{"record":{"id":"9383e0bcea56f883","repo":"slackhq/nebula","slug":"erralreadystopped","errorCode":"ErrAlreadyStopped","errorMessage":"nebula cannot be restarted","messagePattern":"nebula cannot be restarted","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"control.go","lineNumber":29,"sourceCode":"\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\n","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/control.go#L11-L47","documentation":"ErrAlreadyStopped in control.go means the nebula control cannot be restarted: once Start's state machine sees StateStopped or StateStopping it returns this error ('nebula cannot be restarted'). Instances are single-shot — a stopped Control is terminal, and Stop is a harmless no-op afterwards.","triggerScenarios":"Calling Start() on an instance that was previously Stop()ed or is mid-stop (control.go:81); exercised in TestControl_StopBeforeStart, TestControl_ConcurrentStopAndStart, TestControl_StartStopLifecycle, control_lifecycle_test.go:113.","commonSituations":"Graceful-shutdown handlers that try to restart after SIGTERM cleanup; watchdogs re-Start()ing a stopped instance instead of constructing a new one; tests reusing a fixture after teardown.","solutions":["Create a fresh Control instance instead of restarting the stopped one.","Guard with errors.Is(err, ErrAlreadyStopped) and treat as expected during shutdown paths.","Track lifecycle so restart attempts are never issued once Stop has been called.","For restart semantics, tear down fully and rebuild via the original config/loader."],"exampleFix":"// before\nc.Stop()\nc.Start() // ErrAlreadyStopped\n\n// after\nc.Stop()\nc = control.New(...)\nc.Start()","handlingStrategy":"try-catch","validationCode":"if c.State() == control.StateStopped || c.State() == control.StateStopping {\n    return errors.New(\"control already stopped; create a new instance to restart\")\n}","typeGuard":"func isRestartable(c *control.Control) bool {\n    s := c.State()\n    return s != control.StateStopped && s != control.StateStopping\n}","tryCatchPattern":"if err := c.Start(); err != nil {\n    if errors.Is(err, control.ErrAlreadyStopped) {\n        return newControlFromConfig(cfg).Start()\n    }\n    return err\n}","preventionTips":["Treat Control as single-use; rebuild from config for restarts.","Suppress restart attempts during shutdown handlers.","Track a 'terminated' flag in your supervisor so watchdogs recreate instances.","Rely on errors.Is(err, ErrAlreadyStopped) to make teardown paths no-op-safe."],"tags":["lifecycle","state-machine","go"],"backgroundTag":"cannot-restart-after-stop","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"}