Skip to content

fix(elasticsearch): gate rolling restarts on cluster health only, not on shard relocations - #187

Merged
Oddly merged 3 commits into
mainfrom
fix/rolling-restart-gate-blocks-on-rebalance
Aug 8, 2026
Merged

fix(elasticsearch): gate rolling restarts on cluster health only, not on shard relocations#187
Oddly merged 3 commits into
mainfrom
fix/rolling-restart-gate-blocks-on-rebalance

Conversation

@Oddly

@Oddly Oddly commented Aug 8, 2026

Copy link
Copy Markdown
Owner

The config-triggered rolling restart gated on relocating_shards == 0 and initializing_shards == 0 on top of the health status. A green cluster can relocate shards indefinitely (the balancer keeps cluster_concurrent_rebalance moves in flight until its backlog is drained), so the gate turned into wait-for-the-balancer and timed out into the rescue path on busy clusters — measured over two hours of waiting on a green, fully redundant 7-node cluster.

Green already guarantees every primary and replica is allocated; a relocation aborted by a restart is retried I/O, not lost redundancy. All three gates (pre-restart, post-restart, cleanup via surviving peer) now check only elasticsearch_config_restart_wait_status.

Covered in tests/integration/rolling_restart_contract.yml: the fake API gained a --relocating flag and a section that asserts the second node is still processed while the cluster reports green with relocating shards. Fails without the fix on the pre-restart gate.

Part of the discussion in #185.

Summary by CodeRabbit

  • Bug Fixes

    • Rolling restarts now proceed when the cluster reaches the configured health status, even if shards are relocating or initializing.
    • Improved handling of rolling restarts during ongoing shard relocation.
  • Tests

    • Added integration coverage for rolling restarts with relocating shards.
    • Enhanced test scenarios to simulate shard relocation and verify cluster-settings requests.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Oddly, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d175ac0-26d4-4093-a9cc-d295aaddb11d

📥 Commits

Reviewing files that changed from the base of the PR and between fcb03a7 and 1a28f8c.

📒 Files selected for processing (3)
  • .github/workflows/test_contracts.yml
  • tests/fakes/fake_es_rolling_api.py
  • tests/integration/rolling_restart_contract.yml
📝 Walkthrough

Walkthrough

The rolling restart waits now require only the configured cluster health status. The fake API can report relocating shards, and an integration contract verifies rolling restart requests while relocation is ongoing.

Changes

Rolling restart relocation handling

Layer / File(s) Summary
Fake API relocation state
tests/fakes/fake_es_rolling_api.py
The fake API accepts --relocating and reports the configured count for relocating and initializing shards.
Rolling health wait conditions
roles/elasticsearch/tasks/restart_and_verify_elasticsearch_rolling_node.yml
All three health waits check only the configured cluster health status.
Relocating shards integration contract
tests/integration/rolling_restart_contract.yml
The contract starts the fake API with two relocating shards, runs the rolling restart, verifies settings requests on both nodes, and resets the log.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RollingRestart as Rolling restart task
  participant FakeAPI as Fake Elasticsearch API
  participant NodeA as Elasticsearch node A
  participant NodeB as Elasticsearch node B
  RollingRestart->>FakeAPI: Read green health with relocating shards
  RollingRestart->>NodeA: Send cluster-settings PUT
  RollingRestart->>NodeB: Send cluster-settings PUT
  FakeAPI-->>RollingRestart: Return configured green health
Loading

Possibly related PRs

  • Oddly/elasticstack#137: Introduced the rolling-restart implementation extended by these health checks and integration tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rolling restarts now depend on cluster health, not shard relocation status.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rolling-restart-gate-blocks-on-rebalance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/rolling_restart_contract.yml`:
- Around line 226-233: Update the until condition in the “Wait for fake API”
task to require HTTP status 200 and both relocating_shards and
initializing_shards equal 2, using the registered _fake_api_health response
fields. Apply the same condition to the corresponding repeated wait block.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a8812e4-494c-47fc-bc85-6976de0bd698

📥 Commits

Reviewing files that changed from the base of the PR and between edfc141 and fcb03a7.

📒 Files selected for processing (3)
  • roles/elasticsearch/tasks/restart_and_verify_elasticsearch_rolling_node.yml
  • tests/fakes/fake_es_rolling_api.py
  • tests/integration/rolling_restart_contract.yml

Comment thread tests/integration/rolling_restart_contract.yml
@Oddly
Oddly force-pushed the fix/rolling-restart-gate-blocks-on-rebalance branch from fcb03a7 to 369c768 Compare August 8, 2026 08:40
Oddly added a commit that referenced this pull request Aug 8, 2026
…ew nits

The PR body promised a contract test at
tests/integration/node_maintenance_contract.yml but the file was never
in the diff. This adds it (unblocked via the same .gitignore whitelist
the other contract tests use), plus a couple of nits the review
flagged:

- node_maintenance_contract.yml: three plays against the fake ES API.
  Play 1 asserts the drain sequence hits every expected endpoint
  (voting exclusion for the correct nodename, allocation=primaries,
  ML upgrade mode ON, boost applied at 500mb, flush), then that the
  restore reverses each one (allocation reset, ML OFF, voting exclusion
  DELETE, baseline restored to 200mb from elasticsearch_cluster_settings,
  boosted-but-baseline-less keys explicitly nulled).
  Play 2 pins elasticsearch_ml_enabled: false and asserts no
  _ml/set_upgrade_mode call fires — catches the ML gating consistency
  fix below.

- Docs example: elasticsearch_maintenance_wait_for_node now matches
  the internal _cat/nodes?h=name lookup by using elasticsearch_nodename
  | default(inventory_hostname). The old value silently spun 60x10s
  when node.name diverged from inventory_hostname and swallowed the fail.

- ML gating: both node_maintenance_start and _end now guard the ML
  upgrade_mode call with when: elasticsearch_ml_enabled | bool,
  matching elasticsearch-rolling-upgrade.yml.

- Voting exclusion: added a comment explaining why the maintenance
  entry points use it even though rolling-upgrade doesn't — the
  reboot/OS-patch window can be long, and the exclusion is always
  cleared in node_maintenance_end.

Duplication with elasticsearch-rolling-upgrade.yml stays as-is — the
sibling PRs #187 and #188 change the same shape of code but in the
rolling-upgrade file, so unifying now would collide messily. Leaving
the two implementations aligned by convention (both gate on status only,
both take an operator-choosable wait_status) and opening a follow-up
to fold rolling-upgrade onto these entry points after the current PR
train settles.
…contract

The 'cleanup via surviving peer on failure' play used a block/rescue to
absorb the simulated es1 rejoin failure. Rescue runs across every host
in the play, so when the include short-circuited es1 with a real
failure, es2 hit the sanity fail() task with no rescue path (the rescue
was flagged run_once: true and delegated back to es1) and the whole
play failed.

The 'Verify cleanup used surviving peer' play right after this already
asserts against the fake API log that the DELETE landed on port 19201
(es2), so the sanity fail() was redundant. Just ignore_errors on the
include and let the log assertion do the work.
@Oddly Oddly added the ci:run Trigger gated pull request CI label Aug 8, 2026
@github-actions github-actions Bot removed the ci:run Trigger gated pull request CI label Aug 8, 2026
…y, drop the sanity assert

Take 3 on this — ignore_errors: true on include_tasks doesn't
propagate into the included tasks reliably (the role's explicit
ansible.builtin.fail with startup diagnostics still counted as a hard
play failure).

Go back to block/rescue, but keep the fix that started this thread: no
sanity fail() task after the include. The following play already
asserts against the fake API log that cleanup went via the surviving
peer — that's the actual contract we want to prove, and it doesn't
need a redundant intra-block trap that lit up es2 with no rescue path
of its own.
@Oddly Oddly added the ci:run Trigger gated pull request CI label Aug 8, 2026
@github-actions github-actions Bot removed the ci:run Trigger gated pull request CI label Aug 8, 2026
@Oddly
Oddly merged commit 6c2dcea into main Aug 8, 2026
18 of 19 checks passed
@Oddly
Oddly deleted the fix/rolling-restart-gate-blocks-on-rebalance branch August 8, 2026 09:28
Oddly added a commit that referenced this pull request Aug 10, 2026
…ew nits

The PR body promised a contract test at
tests/integration/node_maintenance_contract.yml but the file was never
in the diff. This adds it (unblocked via the same .gitignore whitelist
the other contract tests use), plus a couple of nits the review
flagged:

- node_maintenance_contract.yml: three plays against the fake ES API.
  Play 1 asserts the drain sequence hits every expected endpoint
  (voting exclusion for the correct nodename, allocation=primaries,
  ML upgrade mode ON, boost applied at 500mb, flush), then that the
  restore reverses each one (allocation reset, ML OFF, voting exclusion
  DELETE, baseline restored to 200mb from elasticsearch_cluster_settings,
  boosted-but-baseline-less keys explicitly nulled).
  Play 2 pins elasticsearch_ml_enabled: false and asserts no
  _ml/set_upgrade_mode call fires — catches the ML gating consistency
  fix below.

- Docs example: elasticsearch_maintenance_wait_for_node now matches
  the internal _cat/nodes?h=name lookup by using elasticsearch_nodename
  | default(inventory_hostname). The old value silently spun 60x10s
  when node.name diverged from inventory_hostname and swallowed the fail.

- ML gating: both node_maintenance_start and _end now guard the ML
  upgrade_mode call with when: elasticsearch_ml_enabled | bool,
  matching elasticsearch-rolling-upgrade.yml.

- Voting exclusion: added a comment explaining why the maintenance
  entry points use it even though rolling-upgrade doesn't — the
  reboot/OS-patch window can be long, and the exclusion is always
  cleared in node_maintenance_end.

Duplication with elasticsearch-rolling-upgrade.yml stays as-is — the
sibling PRs #187 and #188 change the same shape of code but in the
rolling-upgrade file, so unifying now would collide messily. Leaving
the two implementations aligned by convention (both gate on status only,
both take an operator-choosable wait_status) and opening a follow-up
to fold rolling-upgrade onto these entry points after the current PR
train settles.
Oddly added a commit that referenced this pull request Aug 10, 2026
…ML doesn't see the JSON colons

The search() patterns held "method": "POST" etc. as bare plain
scalars, which YAML treated as key: value mappings mid-item and
rejected the file with 'did not find expected key'. Fixed by wrapping
each list item in double quotes and escaping the inner double quotes.

The file wasn't picked up by contracts CI until PR #187 landed; that's
why the parse error only surfaced on the rebase pass.
Oddly added a commit that referenced this pull request Aug 11, 2026
…rchestration (#190)

* feat(elasticsearch): add node maintenance entry points for external orchestration

* fix(elasticsearch): make node maintenance entry points independent of the elasticstack role defaults

* test(elasticsearch): add node_maintenance_contract.yml + address review nits

The PR body promised a contract test at
tests/integration/node_maintenance_contract.yml but the file was never
in the diff. This adds it (unblocked via the same .gitignore whitelist
the other contract tests use), plus a couple of nits the review
flagged:

- node_maintenance_contract.yml: three plays against the fake ES API.
  Play 1 asserts the drain sequence hits every expected endpoint
  (voting exclusion for the correct nodename, allocation=primaries,
  ML upgrade mode ON, boost applied at 500mb, flush), then that the
  restore reverses each one (allocation reset, ML OFF, voting exclusion
  DELETE, baseline restored to 200mb from elasticsearch_cluster_settings,
  boosted-but-baseline-less keys explicitly nulled).
  Play 2 pins elasticsearch_ml_enabled: false and asserts no
  _ml/set_upgrade_mode call fires — catches the ML gating consistency
  fix below.

- Docs example: elasticsearch_maintenance_wait_for_node now matches
  the internal _cat/nodes?h=name lookup by using elasticsearch_nodename
  | default(inventory_hostname). The old value silently spun 60x10s
  when node.name diverged from inventory_hostname and swallowed the fail.

- ML gating: both node_maintenance_start and _end now guard the ML
  upgrade_mode call with when: elasticsearch_ml_enabled | bool,
  matching elasticsearch-rolling-upgrade.yml.

- Voting exclusion: added a comment explaining why the maintenance
  entry points use it even though rolling-upgrade doesn't — the
  reboot/OS-patch window can be long, and the exclusion is always
  cleared in node_maintenance_end.

Duplication with elasticsearch-rolling-upgrade.yml stays as-is — the
sibling PRs #187 and #188 change the same shape of code but in the
rolling-upgrade file, so unifying now would collide messily. Leaving
the two implementations aligned by convention (both gate on status only,
both take an operator-choosable wait_status) and opening a follow-up
to fold rolling-upgrade onto these entry points after the current PR
train settles.

* test(elasticsearch): quote node-maintenance contract assertions so YAML doesn't see the JSON colons

The search() patterns held "method": "POST" etc. as bare plain
scalars, which YAML treated as key: value mappings mid-item and
rejected the file with 'did not find expected key'. Fixed by wrapping
each list item in double quotes and escaping the inner double quotes.

The file wasn't picked up by contracts CI until PR #187 landed; that's
why the parse error only surfaced on the rebase pass.

* test(elasticsearch): fix node_maintenance_contract regex order to match the fake API log

The fake API emits {"body":…, "method":…, "path":…, "port":…}
alphabetically per request, so content that lives in the body (primaries,
500mb, null values) shows up in the log line BEFORE the "method"/"path"
markers. The old assertions expected the opposite ordering and could not
match under regex '.' semantics (which don't cross newlines). Verified
against a copy of the actual fake API log dump from the failed run.

---------

Co-authored-by: Sam Crauwels <scrauwels@rinis.nl>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant