hashicorp/consul · error

ClusterSize not set

Error message

ClusterSize not set

What it means

agent/ae.StateSyncer drives Consul's anti-entropy full/partial sync state machine. Run() requires the ClusterSize callback (used at ae.go:321 to scale the sync rate) to be set; the Consul agent wires it in agent/agent.go:761 to the LAN Serf member count. Calling Run() on a StateSyncer whose ClusterSize field is nil panics immediately — a fail-fast guard against mis-wiring, not a runtime data condition.

Source

Thrown at agent/ae/ae.go:162

// fsmState defines states for the state machine.
type fsmState string

const (
	doneState          fsmState = "done"
	fullSyncState      fsmState = "fullSync"
	partialSyncState   fsmState = "partialSync"
	retryFullSyncState fsmState = "retryFullSync"
)

// Run is the long running method to perform state synchronization
// between local and remote servers.
func (s *StateSyncer) Run() {
	if s.Disabled() {
		return
	}
	if s.ClusterSize == nil {
		panic("ClusterSize not set")
	}
	s.resetNextFullSyncCh()
	s.runFSM(fullSyncState, s.nextFSMState)
}

// runFSM runs the state machine.
func (s *StateSyncer) runFSM(fs fsmState, next func(fsmState) fsmState) {
	for {
		if fs = next(fs); fs == doneState {
			return
		}
	}
}

// nextFSMState determines the next state based on the current state.
func (s *StateSyncer) nextFSMState(fs fsmState) fsmState {
	switch fs {
	case fullSyncState:

View on GitHub (pinned to 2397ff0d76)

Solutions

  1. Set the ClusterSize callback before starting the syncer: sync.ClusterSize = func() int { return memberCount() }
  2. If embedding, mirror the agent wiring (agent/agent.go:761): use your LAN Serf member count, e.g. len(serfLAN.Members())
  3. In tests, use a fixed value like ClusterSize = func() int { return 1 } (pattern from ae_test.go:413)

Example fix

// before
s := &ae.StateSyncer{Logger: logger, StateStore: store}
go s.Run() // panic: ClusterSize not set

// after
s := &ae.StateSyncer{Logger: logger, StateStore: store}
s.ClusterSize = func() int { return len(serfLAN.Members()) }
go s.Run()
Defensive patterns

Strategy: validation

Validate before calling

// before starting the syncer
if syncer.ClusterSize == nil {
    return errors.New("ae.StateSyncer.ClusterSize must be set before Run (e.g. len(serfLAN.Members()))")
}
go syncer.Run()

Try / catch

// test-harness only: assert the fail-fast contract instead of crashing
func mustRun(s *ae.StateSyncer) (r any) {
    defer func() { r = recover() }()
    go s.Run()
    time.Sleep(50 * time.Millisecond)
    return nil
}

Prevention

When it happens

Trigger: Constructing ae.StateSyncer directly (unit tests, embedded Consul, custom agent wiring) and calling Run() without setting ClusterSize; refactoring that drops the field assignment; test setup copied from a fixture that omits it (ae_test.go:132 explicitly asserts this panic).

Common situations: Embedding Consul's anti-entropy syncer into another binary; upgrading Consul versions where StateSyncer gained required fields; writing new unit tests that build the struct with only partial fields.

Related errors


AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15). Data as JSON: /api/errors/8250453c518f5689. Report an issue: GitHub.