slackhq/nebula · warning

ErrAlreadyStarted

ErrAlreadyStarted

Error message

nebula is already started

What it means

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.

Source

Thrown at control.go:28

	"sync"
	"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. Check c.State() (or track lifecycle yourself) before calling Start.
  2. Treat ErrAlreadyStarted as a no-op success: use errors.Is(err, ErrAlreadyStarted) to skip instead of failing.
  3. Restructure startup to a single idempotent entry point guarded by sync.Once or a mutex.
  4. If a restart is intended, call Stop() first and wait for StateStopped.

Example fix

// before
c.Start()
...
c.Start() // panics flow with ErrAlreadyStarted

// after
if err := c.Start(); err != nil && !errors.Is(err, ErrAlreadyStarted) {
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

if c.State() == control.StateStarted {
    // already running; skip Start
    return nil
}

Type guard

func canStart(c *control.Control) bool {
    return c.State() == control.StateReady
}

Try / catch

if err := c.Start(); err != nil {
    if errors.Is(err, control.ErrAlreadyStarted) {
        return nil // idempotent
    }
    return err
}

Prevention

When it happens

Trigger: Calling c.Start() twice without an intervening Stop (control.go:83); asserted in TestControl_StartStopLifecycle and control_lifecycle_test.go:277.

Common situations: Supervisor/systemd double-start attempts, retry logic that re-invokes Start on ambiguous failure, tests or hot-reload paths starting an already-running instance.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/c98782418f93b9d3. Report an issue: GitHub.