Level 4: Cluster Coordination and State

This is the hardest level in the curriculum so far, and one of the hardest in the whole book. Say it plainly: the coordination layer is where strong engineers get humbled. It is a distributed consensus protocol (Zen2 — a Raft-like algorithm), an immutable versioned state object, a two-phase publish/commit protocol, two cooperating single-threaded services, and a shard allocation engine — all interacting. You will not understand it in one pass. You will read it, run it, watch the logs, re-read it, and only then will the picture cohere.

This level is the OpenSearch analog of Tez's state-machine internals (VertexImpl/DAGImpl). Where Tez has a DAG state machine driven by an async dispatcher, OpenSearch has a cluster state driven by consensus and applied through a publish protocol. Both reward the same approach: read the state object, read the transitions, run it, watch it move.

Warning: Do not start Level 4 until you can trace REST → Transport → Action without a guide (Level 3). The coordination layer assumes you know what a TransportAction, Writeable, and the cluster manager (formerly master) are.


Learning Objectives

By the end of Level 4 you must be able to:

  1. Explain the coordination layer: the Coordinator, election, joins, and publish/commit, and how it forms and maintains a cluster.
  2. Describe ClusterState as an immutable, versioned snapshot and name its four major components: Metadata, RoutingTable, DiscoveryNodes, ClusterBlocks.
  3. Distinguish the two services: the cluster-manager service (MasterService) that computes new states from update tasks, and the ClusterApplierService that applies committed states.
  4. Trace a cluster state update from a ClusterStateUpdateTask through computation, two-phase publication, and application — and explain why each phase exists.
  5. Distinguish a ClusterStateApplier from a ClusterStateListener and say when to use each.
  6. Explain shard allocation: AllocationService, RoutingAllocation, and the AllocationDeciders chain, and read a single decider end to end.
  7. Read and reason about org.opensearch.cluster.coordination and org.opensearch.cluster.routing.allocation without a guide.

The Coordination Layer

Everything in OpenSearch that is "the cluster agreeing on something" runs through org.opensearch.cluster.coordination. The protagonist is the Coordinator: a per-node object that implements OpenSearch's consensus (called Zen2, a Raft-inspired protocol that replaced the original "Zen" discovery). It owns election, joins, failure detection, and publication.

find server/src/main/java -path "*cluster/coordination*" -name "*.java" | sort

The cast you will meet in Lab 4.1:

ClassResponsibility
CoordinatorThe orchestrator: discovery, election, joins, publication, failure handling
CoordinationStateThe persisted consensus state: term, last-accepted/committed state, voting config
PreVoteCollectorPre-voting round (avoids disruptive elections before a real vote)
ElectionSchedulerFactoryRandomized, backing-off election timers (prevents thundering herds)
JoinHelperSends/receives join requests; a follower joins an elected manager
FollowersCheckerThe manager pings followers; removes ones that stop responding
LeaderCheckerA follower pings the manager; triggers a new election if it disappears
ClusterFormationFailureHelperProduces the "cluster not formed; reason: ..." diagnostics
PublicationTransportHandlerThe transport handler for two-phase publish then commit

Consensus in one paragraph. A cluster-manager-eligible node that thinks there is no leader starts a pre-vote (PreVoteCollector). If it gathers enough pre-votes, it bumps the term and starts a real election, collecting join votes from a quorum of the voting configuration (CoordinationState.VoteCollection). Once it has a quorum it becomes the elected manager, and from then on it is the only node allowed to compute and publish new cluster states. Quorum-based voting is what prevents split-brain: with a voting config of N nodes, you need ⌊N/2⌋+1 votes, so two disjoint majorities cannot both exist.

The first cluster ever formed needs a seed: cluster.initial_cluster_manager_nodes (deprecated alias: cluster.initial_master_nodes) names the bootstrap voting configuration. You set it once, on first formation, and never again — it is ignored after the cluster has a committed state.

See the discovery & coordination deep dive and the cluster state publishing deep dive.


ClusterState: The Immutable Versioned Snapshot

ClusterState is the single source of truth about the cluster. It is immutable: every change produces a new ClusterState with an incremented version. The elected cluster manager publishes it to every node; all nodes converge on the same versioned snapshot.

find server/src/main/java -name "ClusterState.java"
grep -n "public.*version\|Metadata\|RoutingTable\|DiscoveryNodes\|ClusterBlocks\|Builder\|long version" \
  server/src/main/java/org/opensearch/cluster/ClusterState.java | head -40

Its four major components:

ComponentClassWhat it holds
MetadataMetadataIndex settings/mappings/aliases, templates, persistent settings, custom metadata
Routing tableRoutingTableWhich shard copies (primary/replica) are assigned to which node, and their state
NodesDiscoveryNodesThe set of nodes (id, name, address, roles), and who is the cluster manager
BlocksClusterBlocksCluster/index-level blocks (e.g. read-only, no-cluster-manager)

Two more properties matter constantly:

  • version — a monotonically increasing number per published state. "The state moved from v42 to v43" is the unit of progress you will watch in logs.
  • stateUUID — a unique id for this exact state; used to validate diffs.

Because publishing the whole state every time would be expensive, OpenSearch ships diffs: a node already on v42 receives only the Diff to reach v43 (Diffable/Diff). Full treatment in Lab 4.2 and the cluster state deep dive.


The Two Services: Compute vs Apply

OpenSearch separates deciding a new state from applying it. Two services, both effectively single-threaded, both hung off ClusterService:

ServiceClassRuns whereJob
Cluster-manager serviceMasterService (a.k.a. cluster-manager service)only on the elected managerBatches ClusterStateUpdateTasks, computes the next ClusterState, publishes it
Applier serviceClusterApplierServiceevery nodeApplies each committed ClusterState to local components
find server/src/main/java -name "MasterService.java" -o -name "ClusterApplierService.java" -o -name "ClusterService.java"
grep -n "submitStateUpdateTask\|runTasks\|calculateTaskOutputs\|publish" \
  server/src/main/java/org/opensearch/cluster/service/MasterService.java | head

Why split them? Only the manager may compute a new state (consensus authority). But every node must apply the committed state to its own services (routing, mappings, settings, allocation). Splitting compute (manager-only) from apply (all nodes) is what lets the same state object drive the whole cluster.

A state change always flows: update task → compute (manager) → publish → commit → apply (all nodes). The next section diagrams it.


A Cluster State Update, End to End

flowchart TD
    SRC[Trigger: PUT _cluster/settings,<br/>create index, node join, shard event] --> TASK[submitStateUpdateTask<br/>ClusterStateUpdateTask]
    TASK --> BATCH[MasterService batches tasks<br/>by ClusterStateTaskExecutor]
    BATCH --> COMP[executor.execute oldState -> newState<br/>version++ , new stateUUID]
    COMP --> PUB1[PublicationTransportHandler<br/>PHASE 1: PUBLISH diff to all nodes]
    PUB1 -->|each node validates & ACKs| QUORUM{quorum of<br/>cluster-manager nodes<br/>ACKed?}
    QUORUM -->|no| FAIL[publication fails;<br/>state NOT applied; retry/step down]
    QUORUM -->|yes| PUB2[PHASE 2: COMMIT to all nodes]
    PUB2 --> APPLY[each node: ClusterApplierService.applyState]
    APPLY --> AP[ClusterStateAppliers run first<br/>routing, mappings, allocation]
    AP --> LIS[ClusterStateListeners run after<br/>observers, plugins, your listener]
    LIS --> ACKD[task listeners ACKed; update complete]

The two phases are the crux:

  1. Publish (phase 1): the manager sends the new state (as a diff) to every node. Each node validates it and ACKs, but does not apply it yet. The manager waits for a quorum of cluster-manager-eligible nodes to ACK.
  2. Commit (phase 2): once a quorum has ACKed, the manager sends a commit. Now every node applies the state via ClusterApplierService.

If the quorum is not reached, the state is never committed and never applied — this is what keeps a partitioned manager from unilaterally changing the cluster. On apply, appliers run before listeners: appliers (ClusterStateApplier) make the node's components consistent with the new state (and may fail the apply); listeners (ClusterStateListener) are fire-and-forget observers.

Both halves are covered in Lab 4.2.


Shard Allocation

The routing table doesn't compute itself. When shards are unassigned (new index, node left, restored snapshot), the AllocationService runs to place them, gated by a chain of AllocationDeciders.

find server/src/main/java -path "*routing/allocation*" -name "AllocationService.java"
find server/src/main/java -path "*routing/allocation/decider*" -name "*.java" | sort
ClassRole
AllocationServiceEntry point: reroute(...), applyStartedShards(...), applyFailedShards(...)
RoutingAllocationThe mutable working context for one allocation round (routing nodes, deciders, state)
RoutingNodesMutable view of shard→node assignment being computed
AllocationDecidersThe ordered chain of deciders; each returns a Decision
BalancedShardsAllocatorThe default balancer that proposes moves within what deciders allow
DecisionYES / NO / THROTTLE with an explanation string

Common deciders you will meet (and fix in Lab 4.4):

DeciderPrevents
SameShardAllocationDeciderPutting a primary and its replica on the same node
MaxRetryAllocationDeciderEndlessly retrying a shard that keeps failing to allocate
DiskThresholdDeciderAllocating onto nodes over the disk watermark
AwarenessAllocationDeciderViolating zone/rack awareness
FilterAllocationDeciderIgnoring include/exclude/require allocation filters
ThrottlingAllocationDeciderToo many concurrent recoveries on a node

A Decision is the unit of correctness here. A wrong Decision.YES/NO, or an unhelpful explanation, is exactly the kind of bug Lab 4.4 fixes. See the shard allocation deep dive.


Key Classes Quick Reference

ClassPackageRole
Coordinatororg.opensearch.cluster.coordinationConsensus: discovery, election, joins, publication
CoordinationStateorg.opensearch.cluster.coordinationPersisted term, accepted/committed state, voting config
JoinHelper / PreVoteCollectororg.opensearch.cluster.coordinationJoins and pre-voting
FollowersChecker / LeaderCheckerorg.opensearch.cluster.coordinationBidirectional failure detection
PublicationTransportHandlerorg.opensearch.cluster.coordinationTwo-phase publish then commit
ClusterStateorg.opensearch.clusterThe immutable, versioned snapshot
Metadata / RoutingTable / DiscoveryNodes / ClusterBlocksorg.opensearch.cluster.*The four state components
MasterServiceorg.opensearch.cluster.serviceCluster-manager service: computes states from tasks
ClusterApplierServiceorg.opensearch.cluster.serviceApplies committed states on every node
ClusterServiceorg.opensearch.cluster.serviceTies the two services together; addListener, submitStateUpdateTask
ClusterStateUpdateTask / ClusterStateTaskExecutororg.opensearch.clusterA state change + how a batch of them computes a new state
ClusterStateApplier / ClusterStateListenerorg.opensearch.clusterApply hooks (consistency) vs observe hooks (reactions)
AllocationService / RoutingAllocation / AllocationDecidersorg.opensearch.cluster.routing.allocationShard placement and its gating

The Labs

LabTitleType
4.1Read the Coordinator and Cluster-Manager ElectionCode-reading (90 min trace)
4.2ClusterState, Publishing, and the Applier ModelReading + live TRACE logging
4.3Build It — A Custom ClusterStateListener / Custom MetadataBuild-it project
4.4Fix It — An AllocationDecider Edge CaseFix-it lab with a diff

Deliverables

You must demonstrate all of the following before advancing to Level 5:

  • A 90-minute guided trace from node start → election → first published state, with class/file citations (Lab 4.1).
  • A _cluster/settings change watched live in TRACE logs, with the state version incrementing (Lab 4.2).
  • A working plugin whose ClusterStateListener logs index create/delete events, exercised under a node test (Lab 4.3).
  • A fixed AllocationDecider with a passing unit test and a _cluster/allocation/explain reproduction (Lab 4.4).
  • From memory: the four components of ClusterState, the two services, and the two phases of publication.

Common Mistakes

MistakeConsequenceFix
Mutating a ClusterState in placeCompile/illegal-state errors; you misread the modelState is immutable; build a new one via ClusterState.builder(oldState)
Confusing ClusterStateApplier with ClusterStateListenerWrong extension point; missed consistency requirementAppliers ensure consistency (run first, may fail apply); listeners only observe
Thinking publish == commitMisunderstand partition behaviorTwo phases: publish+ACK (quorum), then commit; uncommitted states are never applied
Setting cluster.initial_cluster_manager_nodes on a running clusterNo effect; confusionIt seeds only the first bootstrap; ignored once a state is committed
Reading deciders as booleanYou miss THROTTLEA Decision is YES/NO/THROTTLE; throttling is not rejection
Blocking inside an applier/listenerStalls the single-threaded applier; cluster freezesAppliers/listeners must be fast and non-blocking
Expecting master terminology to be goneYou can't find codemaster survives as deprecated aliases throughout (MasterService, master_timeout)

How to Verify Success

# 1. You can see the current cluster state version and who is manager.
curl -s 'localhost:9200/_cluster/state/version,master_node?pretty'
curl -s 'localhost:9200/_cat/cluster_manager?v'

# 2. You can find any coordination/allocation class by grep, not memory.
find server/src/main/java -path "*cluster/coordination*" -name "Coordinator.java"
find server/src/main/java -path "*routing/allocation/decider*" -name "SameShardAllocationDecider.java"

# 3. You can watch a state version increment.
curl -s -XPUT localhost:9200/_cluster/settings -H 'Content-Type: application/json' \
  -d '{"transient":{"cluster.routing.allocation.enable":"all"}}'
curl -s 'localhost:9200/_cluster/state/version?pretty'   # version went up

When you can explain election, the two services, the two-phase publish, and a single decider — and prove each with a command — you are ready for Level 5: Testing and Debugging.