slackhq/nebula · warning
ErrAlreadyStopped
ErrAlreadyStopped
Error message
nebula cannot be restarted
What it means
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.
Source
Thrown at control.go:29
"syscall"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/overlay"
)
type RunState int
const (
StateUnknown RunState = iota
StateReady
StateStarted
StateStopping
StateStopped
)
var ErrAlreadyStarted = errors.New("nebula is already started")
var ErrAlreadyStopped = errors.New("nebula cannot be restarted")
var ErrUnknownState = errors.New("nebula state is invalid")
// Every interaction here needs to take extra care to copy memory and not return or use arguments "as is" when touching
// core. This means copying IP objects, slices, de-referencing pointers and taking the actual value, etc
type controlEach func(h *HostInfo)
type controlHostLister interface {
QueryVpnAddr(vpnAddr netip.Addr) *HostInfo
ForEachIndex(each controlEach)
ForEachVpnAddr(each controlEach)
GetPreferredRanges() []netip.Prefix
}
type Control struct {
stateLock sync.Mutex
state RunState
View on GitHub (pinned to dd8f660c0a)
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.
Example fix
// before c.Stop() c.Start() // ErrAlreadyStopped // after c.Stop() c = control.New(...) c.Start()
Defensive patterns
Strategy: try-catch
Validate before calling
if c.State() == control.StateStopped || c.State() == control.StateStopping {
return errors.New("control already stopped; create a new instance to restart")
} Type guard
func isRestartable(c *control.Control) bool {
s := c.State()
return s != control.StateStopped && s != control.StateStopping
} Try / catch
if err := c.Start(); err != nil {
if errors.Is(err, control.ErrAlreadyStopped) {
return newControlFromConfig(cfg).Start()
}
return err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/9383e0bcea56f883.
Report an issue: GitHub.