{"record":{"id":"8250453c518f5689","repo":"hashicorp/consul","slug":"clustersize-not-set","errorCode":null,"errorMessage":"ClusterSize not set","messagePattern":"ClusterSize not set","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"agent/ae/ae.go","lineNumber":162,"sourceCode":"\n// fsmState defines states for the state machine.\ntype fsmState string\n\nconst (\n\tdoneState          fsmState = \"done\"\n\tfullSyncState      fsmState = \"fullSync\"\n\tpartialSyncState   fsmState = \"partialSync\"\n\tretryFullSyncState fsmState = \"retryFullSync\"\n)\n\n// Run is the long running method to perform state synchronization\n// between local and remote servers.\nfunc (s *StateSyncer) Run() {\n\tif s.Disabled() {\n\t\treturn\n\t}\n\tif s.ClusterSize == nil {\n\t\tpanic(\"ClusterSize not set\")\n\t}\n\ts.resetNextFullSyncCh()\n\ts.runFSM(fullSyncState, s.nextFSMState)\n}\n\n// runFSM runs the state machine.\nfunc (s *StateSyncer) runFSM(fs fsmState, next func(fsmState) fsmState) {\n\tfor {\n\t\tif fs = next(fs); fs == doneState {\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// nextFSMState determines the next state based on the current state.\nfunc (s *StateSyncer) nextFSMState(fs fsmState) fsmState {\n\tswitch fs {\n\tcase fullSyncState:","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/hashicorp/consul/blob/2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e/agent/ae/ae.go#L144-L180","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Set the ClusterSize callback before starting the syncer: sync.ClusterSize = func() int { return memberCount() }","If embedding, mirror the agent wiring (agent/agent.go:761): use your LAN Serf member count, e.g. len(serfLAN.Members())","In tests, use a fixed value like ClusterSize = func() int { return 1 } (pattern from ae_test.go:413)"],"exampleFix":"// before\ns := &ae.StateSyncer{Logger: logger, StateStore: store}\ngo s.Run() // panic: ClusterSize not set\n\n// after\ns := &ae.StateSyncer{Logger: logger, StateStore: store}\ns.ClusterSize = func() int { return len(serfLAN.Members()) }\ngo s.Run()","handlingStrategy":"validation","validationCode":"// before starting the syncer\nif syncer.ClusterSize == nil {\n    return errors.New(\"ae.StateSyncer.ClusterSize must be set before Run (e.g. len(serfLAN.Members()))\")\n}\ngo syncer.Run()","typeGuard":null,"tryCatchPattern":"// test-harness only: assert the fail-fast contract instead of crashing\nfunc mustRun(s *ae.StateSyncer) (r any) {\n    defer func() { r = recover() }()\n    go s.Run()\n    time.Sleep(50 * time.Millisecond)\n    return nil\n}","preventionTips":["When embedding ae.StateSyncer, copy the agent's wiring pattern from agent/agent.go:761","Centralize StateSyncer construction in one factory that sets every required field","Keep a compile-time checklist of required callbacks (ClusterSize, Logger, StateStore) in the factory"],"tags":["go","consul","anti-entropy","panic","wiring"],"backgroundTag":null,"analyzedSha":"2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e","analyzedAt":"2026-08-15T19:19:47.700Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}