IndexShard Lifecycle
A shard is the unit of storage and the unit of work in OpenSearch. Every
document lives in exactly one shard; every search runs per-shard; every write
applies to a shard's primary first. The Java object that owns a shard on a data
node is IndexShard, and this chapter is about its full lifecycle: how the
hierarchy IndicesService → IndexService → IndexShard is built, the shard state
machine (IndexShardState), the Store/Directory that backs it, how a shard
is created, recovered, started, and closed, and how index/delete operations are
applied on the primary versus a replica. The shard is where the cluster layer
(routing table) meets the storage layer (the engine, the
translog, and refresh/flush/merge).
After this chapter you should be able to: draw the IndexShardState machine;
explain who creates and starts shards in response to cluster-state changes; find
applyIndexOperationOnPrimary/OnReplica and explain why they differ; and name
the components an IndexShard owns.
Note: Distinguish
IndexShardState(the data node's reality for the shard object) fromShardRouting.state()(the cluster manager's plan for the shard, in cluster state — see cluster-and-node-model.md). Both have states; they are not the same machine.
The hierarchy: IndicesService → IndexService → IndexShard
find server -name "IndicesService.java" -o -name "IndexService.java" -o -name "IndexShard.java"
grep -n "class IndicesService\|createIndex\|indexService(" \
server/src/main/java/org/opensearch/indices/IndicesService.java
grep -n "class IndexService\|createShard\|removeShard\|getShard" \
server/src/main/java/org/opensearch/index/IndexService.java
| Level | Class | Scope | Owns |
|---|---|---|---|
| Node | IndicesService | all indices on this node | the map of IndexServices; circuit breakers for indexing; shared caches |
| Index | IndexService | one index on this node | per-index MapperService, SimilarityService, analysis, and the map of local IndexShards |
| Shard | IndexShard | one shard copy | the Engine, the Store, the Translog (via engine), refresh/flush logic, operation permits |
flowchart TD
Node[Node] --> IsS[IndicesService]
IsS --> IxS1["IndexService 'orders'"]
IsS --> IxS2["IndexService 'logs'"]
IxS1 --> MS[MapperService]
IxS1 --> Sh0["IndexShard 0 (primary)"]
IxS1 --> Sh1["IndexShard 1 (replica)"]
Sh0 --> Eng[InternalEngine]
Eng --> IW[Lucene IndexWriter]
Eng --> TL[Translog]
Sh0 --> St[Store -> Directory]
The bridge from cluster state to these objects is IndicesClusterStateService,
an applier (see cluster-state-publishing.md).
When the routing table assigns a shard to this node, the applier asks
IndexService.createShard(...); when it un-assigns one, the applier removes it.
grep -n "createShard\|removeShard\|failAndRemoveShard\|createOrUpdateShards" \
server/src/main/java/org/opensearch/indices/cluster/IndicesClusterStateService.java
The Store and Directory
The bytes of a shard live on disk under the node's data path, wrapped by Store,
which holds a Lucene Directory.
find server -name "Store.java" -path "*index/store*"
grep -n "class Store\|Directory\|directory()\|verify\|MetadataSnapshot\|checkIntegrity" \
server/src/main/java/org/opensearch/index/store/Store.java
| Concept | Class | Role |
|---|---|---|
| Shard storage | Store | Reference-counted wrapper over a Lucene Directory; tracks files, checksums, the segment metadata snapshot used by recovery. |
| Filesystem | Directory (Lucene) | The actual file abstraction (NIOFSDirectory/MMapDirectory via FsDirectoryFactory). |
| File-level integrity | Store.MetadataSnapshot | Per-file checksums/lengths; lets recovery copy only differing files (see recovery.md). |
Store is reference-counted: a shard can be in use by a search while it is being
closed; the refcount keeps the directory alive until all users release it. This
is why a shard close is not instantaneous.
The IndexShardState machine
find server -name "IndexShardState.java"
grep -n "CREATED\|RECOVERING\|POST_RECOVERY\|STARTED\|CLOSED\|enum IndexShardState" \
server/src/main/java/org/opensearch/index/shard/IndexShardState.java
grep -n "changeState\|state =\|IndexShardState\." \
server/src/main/java/org/opensearch/index/shard/IndexShard.java | head
stateDiagram-v2
[*] --> CREATED: IndexService.createShard
CREATED --> RECOVERING: recovery starts (markAsRecovering)
RECOVERING --> POST_RECOVERY: engine opened, ops replayed
POST_RECOVERY --> STARTED: shard marked started, serves traffic
STARTED --> CLOSED: node leaves / shard removed / index closed
RECOVERING --> CLOSED: recovery failed
POST_RECOVERY --> CLOSED: failure
CREATED --> CLOSED: aborted
| State | Meaning | Can serve reads/writes? |
|---|---|---|
CREATED | Object exists; engine not open. | No |
RECOVERING | Filling the shard from a source (empty/store/peer/snapshot). | No |
POST_RECOVERY | Engine open and ops replayed; finalizing. | Not yet announced as started |
STARTED | Live; primary takes writes, copies serve reads. | Yes |
CLOSED | Engine closed, resources released. | No |
The recovery source that fills a RECOVERING shard depends on context:
| Recovery type | Source | Deep dive |
|---|---|---|
| empty store | new primary, no data | — |
| existing store | local Lucene files on this node | — |
| peer | copy from the primary on another node | recovery.md |
| snapshot | restore from a repository | snapshots-repositories.md |
Creating, recovering, starting, closing
sequenceDiagram
participant ICSS as IndicesClusterStateService (applier)
participant IxS as IndexService
participant Sh as IndexShard
participant Eng as Engine
participant CM as Cluster manager
ICSS->>IxS: createShard(shardRouting) [routing assigned here]
IxS->>Sh: new IndexShard (CREATED)
ICSS->>Sh: startRecovery(...)
Sh->>Sh: markAsRecovering (RECOVERING)
Sh->>Eng: openEngineAndRecoverFromTranslog / fillFromSource
Eng-->>Sh: ops replayed (POST_RECOVERY)
Sh->>CM: shard-started action
CM->>CM: cluster-state update: ShardRouting -> STARTED
Note over Sh: applier observes STARTED -> IndexShard STARTED
Trace the key methods:
grep -n "markAsRecovering\|recoverFromStore\|openEngineAndRecover\|postRecovery\|markShardAsStarted\|close(" \
server/src/main/java/org/opensearch/index/shard/IndexShard.java | head -30
Note the round-trip: the data node reports the shard started
(ShardStateAction → a cluster-state update on the manager), and only then does
the routing table mark the ShardRouting STARTED. The two state machines
converge through this handshake.
grep -n "shardStarted\|class ShardStateAction\|sendShardStarted" \
server/src/main/java/org/opensearch/cluster/action/shard/ShardStateAction.java
Applying operations: primary vs replica
A write reaches a shard via TransportReplicationAction
(action-framework.md). The shard has two application
paths, and the difference is fundamental:
grep -n "applyIndexOperationOnPrimary\|applyIndexOperationOnReplica\|applyDeleteOperationOnPrimary\|\
applyDeleteOperationOnReplica\|markSeqNoAsNoop" \
server/src/main/java/org/opensearch/index/shard/IndexShard.java
| Path | Method | Assigns sequence number? | Version logic |
|---|---|---|---|
| Primary | applyIndexOperationOnPrimary | Yes — the primary is the source of truth; it allocates the seq no and resolves versioning. | Resolves the requested version type, checks/sets _version. |
| Replica | applyIndexOperationOnReplica | No — it receives the seq no the primary assigned and applies idempotently. | Trusts the primary's decision; applies at the given seq no. |
flowchart TD
Op[index request] --> Pr{primary or replica?}
Pr -->|primary| AP[applyIndexOperationOnPrimary]
AP --> Seq[assign seq no + primary term]
Seq --> Ver[resolve version conflict]
Ver --> Eng1["InternalEngine.index() -> IndexWriter + Translog.add"]
Eng1 --> Repl[replicate to in-sync replicas]
Repl --> AR[applyIndexOperationOnReplica on each]
AR --> Eng2["InternalEngine.index() at given seq no (idempotent)"]
Why the split matters: the primary decides (seq no, version, conflict
resolution); replicas obey. This makes replication deterministic and lets a
recovering replica replay the primary's exact operation history. The sequence
number machinery (LocalCheckpointTracker, global checkpoint via
ReplicationTracker) is detailed in engine-internals.md
and replication.md.
Warning: A replica must apply operations idempotently at the seq no the primary assigned. If a replica re-derived its own seq no or version, replicas would diverge from the primary — a data-correctness bug, not a performance one.
What IndexShard owns and exposes
The shard is the integration point. The hooks you will meet:
| Hook / component | Purpose | Deep dive |
|---|---|---|
Engine (getEngine()) | The Lucene-backed write/read engine | engine-internals.md |
refresh(...) | Open a new searcher; make recent writes visible | refresh-flush-merge.md |
flush(...) | Lucene commit + translog roll for durability | translog.md, refresh-flush-merge.md |
acquireSearcher(...) | Get a point-in-time Engine.Searcher for a query | search-execution.md |
IndexShardOperationPermits | Serialize/exclude operations | threadpools-concurrency.md |
IndexEventListener | Callbacks on shard lifecycle transitions (plugin hook) | plugin-architecture.md |
IndexEventListener is the extension seam: plugins (and core services) get
notified on beforeIndexShardCreated, afterIndexShardStarted,
beforeIndexShardClosed, etc.
find server -name "IndexEventListener.java"
grep -n "default void\|afterIndexShardStarted\|beforeIndexShardClosed\|onShardInactive" \
server/src/main/java/org/opensearch/index/shard/IndexEventListener.java
Reading exercise
# 1. The state machine.
grep -n "IndexShardState\|changeState\|verifyActive\|verifyNotClosed" \
server/src/main/java/org/opensearch/index/shard/IndexShard.java | head
# 2. Create/recover/start path.
grep -n "createShard\|startRecovery\|markAsRecovering\|recoverFromStore" \
server/src/main/java/org/opensearch/index/IndexService.java \
server/src/main/java/org/opensearch/index/shard/IndexShard.java | head
# 3. Primary vs replica application.
grep -n "applyIndexOperationOnPrimary\|applyIndexOperationOnReplica" \
server/src/main/java/org/opensearch/index/shard/IndexShard.java
# 4. The started handshake.
grep -n "sendShardStarted\|shardStarted" \
server/src/main/java/org/opensearch/cluster/action/shard/ShardStateAction.java
# 5. Tests + live view.
./gradlew :server:test --tests "org.opensearch.index.shard.IndexShardTests"
curl -s "localhost:9200/_cat/shards?v&h=index,shard,prirep,state,docs,node"
Answer:
- Name the three levels of the
IndicesService → IndexService → IndexShardhierarchy and one thing each level owns. - Draw
IndexShardStatefromCREATEDtoSTARTEDandCLOSED. In which states can the shard serve traffic? - Who calls
IndexService.createShard, and what cluster-state change triggers it? - Explain the started handshake: how does a data node's
STARTEDshard cause the routing table'sShardRoutingto becomeSTARTED? - Contrast
applyIndexOperationOnPrimaryandapplyIndexOperationOnReplica. Which assigns the sequence number, and why must the other be idempotent? - What is
IndexEventListenerfor, and name two lifecycle callbacks a plugin could hook?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
Shard stuck in INITIALIZING (never STARTED) | Recovery failing or blocked; or never assigned | _cluster/allocation/explain; recovery.md, shard-allocation.md |
| Replica diverges from primary | Replica path re-derived seq no/version instead of obeying primary | applyIndexOperationOnReplica; replication.md |
IndexShardClosedException mid-request | Op ran while shard closing; refcount/permit handling missing | Store refcount; IndexShardOperationPermits |
| Slow shard close / leaked file handles | Store/Searcher not released; refcount never hits zero | leak detector; acquireSearcher callers |
Data node STARTED but cluster shows INITIALIZING | started handshake message lost or delayed | ShardStateAction.sendShardStarted |
| Plugin shard hook never fires | IndexEventListener not registered via the index module | onIndexModule/listener registration |
Validation: prove you understand this
- Draw the full object graph from
IndicesServicedown to the LuceneIndexWriterandTranslog, naming each owning class. - Reproduce the
IndexShardStatediagram from memory and annotate which states serve reads/writes and which recovery source fillsRECOVERING. - Explain the started handshake between a data node and the cluster manager,
and why two state machines (
IndexShardState,ShardRouting.state) exist. - Explain precisely why the primary assigns sequence numbers and replicas do not, and the data-correctness bug that the asymmetry prevents.
- Explain why
Storeis reference-counted and what a missing release causes. - List two
IndexEventListenercallbacks and a realistic use for each (e.g. a plugin warming caches onafterIndexShardStarted).