Resolving ISM write-block conflicts on follower indices

This is a diagnostic runbook for the specific incident where an Index State Management (ISM) action has already failed against a Cross-Cluster Replication (CCR) follower with a cluster_block_exception — the managed index is parked in Failed, and you need to identify which action hit the write block, unblock any legitimate terminal action, and retry the policy cleanly. It is the recovery counterpart to the preventative design in Follower Policy Design, for on-call engineers staring at a stuck follower rather than authoring a policy from scratch.

The workflow is: read the failure with _plugins/_ism/explain, decide whether the failed action belongs on the leader (most cases) or is a terminal action that just needs replication stopped, act accordingly, then _plugins/_ism/retry to clear the Failed state. Getting the decision right matters — retrying a force_merge on a live follower just fails again, while force-clearing a write block on a still-replicating follower corrupts its checkpoint.

Prerequisites

How a write-block conflict arises

A follower carries index.blocks.write: true, set by the replication engine. When ISM’s worker executes an action that mutates segments or shard topology, the block converts the action into a cluster_block_exception and ISM records the step as failed. The follower keeps replicating; only the policy is stuck. That decoupling is important to internalise before you touch anything: the data on the follower is fine and still current, so there is no urgency to force the action through. The failed action is a policy-authoring bug surfacing at runtime, and the correct response is almost always to fix the policy or stop replication cleanly — never to fight the block.

Two error codes look nearly identical in the ISM explain output but demand opposite fixes. FORBIDDEN/8/index write (api) is the replication-owned write block, cleared only by stopping replication. FORBIDDEN/12/index read-only / allow delete (api) is the disk flood-stage block that OpenSearch sets when a node crosses the flood_stage watermark, cleared by freeing disk and resetting index.blocks.read_only_allow_delete. Misreading a /12 as a /8 sends you to stop replication when the real problem is a full cold tier. The diagram below traces the decision for the CCR write block specifically; the code-check in step 1 tells you which block you are actually looking at.

Resolving a follower write-block conflict Flowchart. Start: read _plugins/_ism/explain for the failed follower. Decision: is the failed action a write action (rollover, force_merge, shrink) or a terminal delete? Write-action branch: move the action to the leader policy and remove it from the follower, then retry. Delete branch: stop replication to lift the write block, then retry. Both branches converge on POST _plugins/_ism/retry. read ISM explain failed action type? write action move action to leader, remove from follower policy terminal delete stop replication, write block lifts POST _ism/retry

Step-by-step procedure

1. Read the failure with ISM explain

Pull the managed-index state and the exact failed action. The info field carries the cluster_block_exception message that names the write block.

Shell
curl -s "https://<follower>:9200/_plugins/_ism/explain/logs-follower-000007?pretty"
Text
{
  "logs-follower-000007": {
    "state": { "name": "warm" },
    "action": { "name": "force_merge", "failed": true },
    "step": { "name": "attempt_call_force_merge", "step_status": "failed" },
    "info": {
      "message": "Failed to start force_merge",
      "cause": "cluster_block_exception ... index [logs-follower-000007] blocked by: [FORBIDDEN/8/index write (api)]"
    }
  }
}

Gotcha: FORBIDDEN/8/index write (api) is the replication-owned write block, not a disk flood-stage read-only block (FORBIDDEN/12). Confirm the block number — a /12 block means a full disk, a different problem entirely.

2. Classify the failed action

Decide which branch you are on. If the failed action is rollover, force_merge, shrink, open, or close, it never belonged on a follower and must move to the leader. If it is delete (or snapshot before delete), it is legitimate but needs replication stopped first.

Shell
# Confirm which actions the follower policy defines, to see if a leader-only action leaked in
curl -s "https://<follower>:9200/_plugins/_ism/explain/logs-follower-000007?show_policy=true&pretty" \
  | grep -oE '"(rollover|force_merge|shrink|delete)"'
Text
"force_merge"
"delete"

Gotcha: seeing force_merge in a follower policy at all confirms a leader-shaped policy was mis-attached. The retry in step 5 will not stick until you remove that action.

3a. Write action — push it to the leader and detach from the follower

For a leaked write action, remove the follower’s current policy and attach the follower-safe one. The write action’s result (merged segments) will replicate down from the leader once you run it there.

HTTP
POST _plugins/_ism/remove/logs-follower-000007
HTTP
POST _plugins/_ism/add/logs-follower-000007
{ "policy_id": "follower_safe_policy" }

Then run the actual merge on the leader, where it is a legal write:

Shell
curl -s -X POST "https://<leader>:9200/logs-000007/_forcemerge?max_num_segments=1"

The merged segments replicate down to the follower through CCR’s normal segment shipping, so the follower ends up with the same one-segment-per-shard layout you wanted — without ISM ever issuing a write against it. This leader-owns-writes discipline is the whole reason the follower policy in Follower Policy Design contains no force_merge: the follower inherits the result rather than performing the action.

3b. Terminal delete — stop replication to lift the block

If the follower is genuinely at end-of-life and the failed action is delete, stop replication. Stopping is what removes the engine’s write block and turns the follower into a plain, deletable index.

Shell
curl -s -X POST "https://<follower>:9200/_plugins/_replication/logs-follower-000007/_stop" \
  -H 'Content-Type: application/json' -d '{}'
Text
{ "acknowledged": true }

Gotcha: stopping is irreversible — a stopped follower cannot resume, only re-bootstrap from the leader. Only do this when the index is about to be deleted, never to “temporarily” push an action through mid-lifecycle.

4. Confirm the write block is gone

Before retrying, verify the block actually lifted. A retry against a still-blocked index just re-fails and burns a retry attempt.

Shell
curl -s "https://<follower>:9200/logs-follower-000007/_settings?filter_path=**.blocks.write"
Text
{}

An empty object means no write block remains. If {"...":{"settings":{"index":{"blocks":{"write":"true"}}}}} still comes back, replication did not fully stop — re-check _plugins/_replication/<follower>/_status.

5. Retry the policy to clear the Failed state

_plugins/_ism/retry re-runs the failed step from where it stopped. It is safe to call repeatedly; it only advances if the underlying block is resolved, mirroring the idempotent retry discipline in Error Handling & Retries.

Shell
curl -s -X POST "https://<follower>:9200/_plugins/_ism/retry/logs-follower-000007?pretty"
Text
{ "updated_indices": 1, "failures": false }

Verification

Confirm the managed index left the Failed state and settled on a follower-safe step.

Shell
curl -s "https://<follower>:9200/_plugins/_ism/explain/logs-follower-000007?pretty"
Text
{
  "logs-follower-000007": {
    "state": { "name": "cold_hold" },
    "action": { "name": "allocation", "failed": false },
    "step": { "name": "attempting_to_apply_allocation", "step_status": "completed" }
  }
}

For the terminal-delete case, confirm the index is actually gone rather than still parked:

Shell
curl -s "https://<follower>:9200/_cat/indices/logs-follower-000007?v"
Text
# (empty) — index removed after replication stop + retry

A healthy result is failed: false on an allocation action for the mid-lifecycle case, or a removed index for the end-of-life case. A recurring failed: true on the same write action means step 3a was skipped — the leaked action is still in the attached policy.

Common failures

Symptom Root cause Fix command
Retry immediately re-fails on force_merge Leader-shaped action still in the follower policy POST _plugins/_ism/remove/<follower> then add follower_safe_policy
_stop returns but write block persists Replication not fully stopped; status still SYNCING Re-check _plugins/_replication/<follower>/_status; wait for PAUSED, then retry
delete retry says index still write-blocked Block is /12 flood-stage read-only, not the CCR block PUT <follower>/_settings {"index.blocks.read_only_allow_delete":null} then retry
Retry succeeds but index re-enters Failed next sweep Policy still contains the write action; retry only cleared the current step Re-author the follower policy per the safe-action table before retrying
security_exception on _stop Service account lacks plugins/replication/index/stop Grant the action per Security & Access Boundaries

Frequently asked questions

Can I just clear `index.blocks.write` to make the failed action succeed?

No. That block is owned by the replication engine. Clearing it manually on a live follower lets a local write land that the leader never saw, which diverges the follower’s checkpoint and forces a full remote resync on the next resume. Move write actions to the leader (3a) or stop replication properly for a terminal delete (3b) — never force the block off underneath live replication.

Does `_plugins/_ism/retry` restart the whole policy or just the failed step?

Just the failed step. It re-attempts the action that recorded failed: true from where it left off, so it does not re-run earlier successful transitions. If the block is still present the retry re-fails without advancing, which is why step 4 verifies the block is gone first.

How do I tell a CCR write block apart from a disk flood-stage block?

Read the block code in the cluster_block_exception message. FORBIDDEN/8/index write (api) is the replication write block resolved by stopping replication; FORBIDDEN/12/index read-only / allow delete (api) is the disk flood-stage block resolved by freeing disk and clearing index.blocks.read_only_allow_delete. They look similar in ISM explain but have completely different fixes.

Up: Follower Policy Design