14 KiB
ClusterConfig Schema Split Analysis
Analysis date: 2026-06-08. Branch: persistent_ha. Source schema:
src/schema/controller.capnp:680, structClusterConfig.
Summary
The ClusterConfig struct (20 fields) mixes three distinct configuration concerns:
cluster-wide business config, HA peer membership, and per-controller performance
tunables. These should be split into separate objects so each can evolve
independently without forcing a re-push of the full config blob to all standbys.
Current State Discovery
Peer list is already structurally separate
The --peers CLI flag (main.c:178-210) parses into myconf.peers_cfg
(xce_cluster_config_t), which is a separate struct from the config blob's
cluster_config_t. At startup, cluster_init_phase1 uses peers_cfg.candidates
to determine my_name, my_port, my_priority, and the peer list — all before
the config blob is loaded (cluster.c:865-880). The config blob's candidates
field is not consulted for HA peer discovery; it is carried for backward
compatibility in the blob but superseded by --peers.
HA tunables are hardcoded at phase1
cluster_init_phase1 uses compile-time defaults for all HA timing parameters
(cluster.c:854-863):
ha_config_t ha_cfg = {
.heartbeat_interval_ms = HA_DEFAULT_HEARTBEAT_INTERVAL_MS,
.heartbeat_timeout_ms = HA_DEFAULT_HEARTBEAT_TIMEOUT_MS,
.election_timeout_ms = HA_DEFAULT_ELECTION_TIMEOUT_MS,
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
...
};
The heartbeatInterval and inactiveHeartbeatCount fields in
ClusterConfig are consumed later (e.g., cluster.c:581-585 writes them into
a config key-value store for runtime access), but the HA init path does not
read them. The heartbeat timeout validation in startup_validation.c:144 works
against the hardcoded defaults, not the config blob values.
vcpus is already deprecated
Field @7 vcpus is marked deprecated in the schema comment: "use --vcpus CLI
flag; kept for config blob backward compatibility." cluster_init_phase2
reads c->myconf->vcpus (set from CLI), not the blob's vcpus field
(cluster.c:1045).
Proposed Split: Three Configuration Objects
┌──────────────────────────────┐
│ Current ClusterConfig │
│ (20 fields) │
└──────────────┬───────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌───────────────────┐ ┌─────────────────┐ ┌──────────────────────┐
│ ClusterConfig │ │ PeersConfig │ │ ControllerConfig │
│ (cluster-wide) │ │ (HA membership) │ │ (per-controller) │
└───────────────────┘ └─────────────────┘ └──────────────────────┘
Group 1: Cluster-Wide Business Config (keep in ClusterConfig)
These fields define the cluster's behavior, resources, and policy. They must
be identical across all controllers. When modified via OP_upgradeConfig,
the new blob is pushed to all standbys and committed on majority.
| Field | Ord | Type | Rationale |
|---|---|---|---|
clusterName |
@0 | Text | Cluster identity — must be identical cluster-wide |
resources |
@8 | List(Resource) | Resource catalog — shared state across all nodes |
queues |
@9 | List(QueueConf) | Queue definitions — cluster scheduling policy |
ugroups |
@10 | List(GroupConfig) | User group membership — access control |
hgroups |
@11 | List(GroupConfig) | Host group membership — scheduling constraints |
hostConfigs |
@12 | List(HostConfig) | Per-host overrides — cluster-wide host knowledge |
hproviders |
@18 | List(ScalerConfig) | Autoscaler provider config — cluster infrastructure |
limits |
@19 | List(LimitConfig) | Resource limits — cluster policy enforcement |
configGeneration |
@20 | UInt64 | Monotonic version counter — replication consistency |
How consumed: cluster_init_phase2 (after election and config load)
iterates these to populate c->resmgr, c->jobmgr, queue tables, host
groups, etc.
Group 2: HA Peer Membership (extract to PeersConfig)
These fields define who is in the cluster. They change independently when controllers are added, removed, or re-IP'd — without any change to the business config.
| Field | Ord | Type | Rationale |
|---|---|---|---|
candidates |
@2 | List(MasterCandidate) | HA peer list — changes independently of business config |
port |
@1 | UInt32 | Fallback listener port — derived from candidates; per-controller in practice |
Already structurally separate: The --peers CLI flag populates
myconf.peers_cfg (xce_cluster_config_t in src/include/clusterconfig.h),
a struct distinct from the config blob's cluster_config_t. The candidates
list in the blob is carried for backward compatibility but is not read for HA
peer discovery at startup. Making the separation formal means:
PeersConfigtravels via its own capnp message type and replication path- Cluster config upgrades via
OP_upgradeConfigno longer need to carry the candidate list - Peer list changes (add/remove controllers) are a dedicated operation with its own quorum semantics
Candidate list has special quorum requirements: Changing the peer set is a
Raft cluster membership change. It must not use the same
repl_wait_majority_ack path as business config because the majority
denominator itself is changing. A separate message type enables a two-phase
membership change protocol (joint consensus) in the future.
Impact of removing candidates from ClusterConfig (verified against codebase):
cluster_build_config() (cluster.c:575–609) is the only place that reads
cluster_config->candidates and cluster_config->ncandidates — it copies them
into the runtime KV store as ncandidates and candidate_N_{name,host,port}.
No other component (scheduler, apiserver, opget, sched) reads those KV keys.
The live peer list used by ha.c:3266–3272 and cluster_open_listener comes
exclusively from peers_cfg.candidates[] (parsed by --peers). Removing
candidates from the blob is therefore a zero-blast-radius change.
| File | Location | Change |
|---|---|---|
src/controller/cluster.c |
587–606 | Remove ncandidates + candidate_N_* KV loop from cluster_build_config() |
src/include/clusterconfig.h |
34–35 | Remove int ncandidates and master_candidate_t** candidates from cluster_config_t |
src/schema/controller.capnp |
683 | Remove candidates @2: List(MasterCandidate) from ClusterConfig |
MasterCandidate fields:
struct MasterCandidate {
name @0: Text; // controller identity (e.g., "ctrl-1")
host @1: Text; // hostname or IP
port @2: UInt32; // peer listener port
}
Group 3: Per-Controller Performance Tunables (extract to ControllerConfig)
These fields configure how an individual controller process behaves. They can legitimately differ across controllers (e.g., a lower-memory node with fewer vcpus, or a WAN-connected standby with a longer heartbeat timeout).
| Field | Ord | Type | Rationale |
|---|---|---|---|
heartbeatInterval |
@3 | UInt32 | Primary heartbeat send interval — can be tuned per primary |
inactiveHeartbeatCount |
@4 | UInt32 | Missed-heartbeat tolerance multiplier |
fetchJobInterval |
@5 | UInt32 | Job fetch poll interval — local scheduler interaction |
netioTimeout |
@6 | UInt32 | Network I/O timeout — environment-specific |
vcpus |
@7 | UInt32 | Already deprecated — use --vcpus CLI flag; remove from schema |
jobmgr |
@13 | HashZArrayConfig | Job manager hash table sizing (nHashSlots, delta) |
workermgr |
@14 | HashZArrayConfig | Worker manager hash table sizing |
qmgr |
@15 | HashZArrayConfig | Queue manager hash table sizing |
resmgr |
@16 | ResmgrConfig | Resource manager hash + instance table sizing |
xstrings |
@17 | HashZArrayConfig | String interning table sizing |
How consumed:
jobmgr/workermgr/qmgr/resmgr/xstringsare read once atcluster_init_phase2to construct the internal key-value stores (cluster.c:803-808). After construction, the config values are not referenced again.heartbeatInterval/inactiveHeartbeatCountare read at runtime by the heartbeat and liveness tasks (cluster.c:1180, 1195).fetchJobIntervaldrives the scheduler polling loop.netioTimeoutis used by the iomgr layer for TCP timeouts.
Why these can differ per controller: A controller on a high-memory machine can be configured with larger hash tables. A controller on a high-latency WAN link can have a longer netioTimeout. These differences do not affect cluster correctness — they only affect local process behavior.
vcpus removal: Already deprecated. The --vcpus CLI flag
(main.c:1045) is the canonical source. Remove field @7 in the next
schema revision.
Fields With Dual Usage (require care)
port (@1)
Decision: Remove. No fallback bind port.
cluster_config->port currently serves as a fallback in cluster_open_listener
(cluster.c:953–975) when --peers does not specify a port for the matching
candidate. This fallback is removed: the --peers port is mandatory.
Affected code (all changes mechanical):
| File | Location | Change |
|---|---|---|
src/controller/cluster.c |
953–975 | Remove the if (my_port == 0) { cluster_config->port } branch; return EINVAL immediately if no port found in peers_cfg |
src/controller/myconf.c |
74 | Remove SET_DEFAULT_IF_UNSET(c->port, 0, 9990) |
src/controller/config_repl.c |
55 | Remove SET_DEFAULT_IF_UNSET(c->port, 0, 9990) |
src/include/clusterconfig.h |
33 | Remove int port from cluster_config_t |
src/schema/controller.capnp |
682 | Remove port @1: UInt32 from ClusterConfig |
Note: "port" in testsuite/test.c:124,266 is the scheduler/worker
connect-to-primary port (via get_setting("port")), not the listener bind
port. It is unaffected.
Implementation Approach
Phase 1: Remove port and candidates from ClusterConfig (immediate)
No backward-compat transition needed for these two fields — the live code
already ignores both (peer discovery uses peers_cfg; port fallback is being
deleted). All changes are mechanical.
src/schema/controller.capnp— Removeport @1andcandidates @2fromClusterConfig; mark or removeMasterCandidatestruct if no longer referenced elsewheresrc/include/clusterconfig.h— Removeint port,int ncandidates,master_candidate_t** candidatesfromcluster_config_tsrc/controller/cluster.c— Removeif (my_port == 0)fallback block incluster_open_listener(lines 967–975); removencandidates+candidate_N_*KV loop incluster_build_config(lines 587–606)src/controller/myconf.candsrc/controller/config_repl.c— RemoveSET_DEFAULT_IF_UNSET(c->port, 0, 9990)fromfill_default_cluster_config- Regenerate capnp bindings
Phase 2: Schema split (wire-compatible)
- Define
PeersConfigandControllerConfigstructs incontroller.capnp - Add them as new fields in the top-level
ConfigDataunion alongside the existingclusterconfig @2 - Regenerate capnp bindings
- Keep the remaining
ClusterConfigfields intact during transition — new fields are additive
Phase 3: Controller reads from new structs
cluster_init_phase2: readControllerConfigfor tunablescluster_config_tretains business config fields only- Fall back to old
ClusterConfigfields when new structs are absent (backward compatibility for existingcluster.binfiles)
Phase 4: Remove superseded tunables from ClusterConfig
- Remove
heartbeatInterval,inactiveHeartbeatCount,fetchJobInterval,netioTimeout,vcpusfromClusterConfig - Remove
jobmgr,workermgr,qmgr,resmgr,xstringsfromClusterConfig
Phase 5 (future): Peer list replication
- Define
OPCode_peersUpdate/OPCode_peersUpdateAckfor peer list replication with joint-consensus semantics - Cluster membership changes become a dedicated operation with its own quorum protocol
Effort and Risk
| Phase | Effort | Risk | Justification |
|---|---|---|---|
| 1 — Remove port + candidates | XS (½d) | Low | No transition needed; dead code removal + schema field removal |
| 2 — Schema split | S (1d) | Low | Additive capnp change; old fields preserved |
| 3 — Read from new structs | M (2-3d) | Medium | Touches cluster_init paths; backward compat needed |
| 4 — Remove superseded tunables | S (1d) | Low | Mechanical removal after Phase 3 is validated |
| 5 — Peer replication | L (1-2wk) | High | New protocol; joint consensus; extensive testing |
References
src/schema/controller.capnp:680-700—ClusterConfigdefinitionsrc/schema/controller.capnp:253—ConfigData.clusterconfig @2src/controller/main.c:178-213—--peersCLI parsing intopeers_cfgsrc/controller/cluster.c:854-880—ha_config_thardcoded defaults + priority derivationsrc/controller/cluster.c:945-977— port derivation from candidates vs config blobsrc/controller/cluster.c:803-808— internal manager constructionsrc/controller/ha.h:134—ha_config_t.heartbeat_interval_msdefault viaha_initsrc/controller/startup_validation.c:133-163— heartbeat config validationsrc/include/clusterconfig.h—xce_cluster_config_t(separate peers struct)