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

  1. Create a fresh Control instance instead of restarting the stopped one.
  2. Guard with errors.Is(err, ErrAlreadyStopped) and treat as expected during shutdown paths.
  3. Track lifecycle so restart attempts are never issued once Stop has been called.
  4. 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

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.