Skip to content

Add a bounded Cluster.health(Duration) overload - #26885

Open
patrickmann wants to merge 3 commits into
masterfrom
fix/cluster-health-bounded-timeout
Open

Add a bounded Cluster.health(Duration) overload#26885
patrickmann wants to merge 3 commits into
masterfrom
fix/cluster-health-bounded-timeout

Conversation

@patrickmann

@patrickmann patrickmann commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

/nocl internal API addition, no user-facing change

Supports Graylog2/graylog-plugin-enterprise#15101 (via Graylog2/graylog-plugin-enterprise#15103).

Description

Cluster.health() cannot be bounded by configuration. Its connect timeout (default 10s) and socket timeout (default 60s) are applied per host, and the REST client works through every configured node inside a single call, so the worst case is a multiple of both. A caller that must return within a deadline of its own has no way to get one today.

This PR adds Cluster.health(java.time.Duration) / ClusterAdapter.health(Duration) overload that issues the request asynchronously and cancels it at the deadline.

Per backend:

  • OS2 and ES7 use PlainActionFuture with the *Async client methods, the same shape cancellableMsearch already uses in both client classes.
  • OS3 routes through the client's existing executeWithClientTimeout, which already implements this design. It simply was not wired into clusterHealth().
  • All three pull the request's own cluster manager timeout down to the same bound.

A timeout is reported the way an unreachable cluster already is, as an empty Optional. To a caller on a deadline, "did not answer in time" and "did not answer" are the same fact. OS3's bounded variant additionally catches the runtime OpenSearchException an error response produces, which its IOException-only handler currently lets escape.

The change is purely additive. The five existing health() call sites are untouched and keep the un-timed behaviour.

Future utility for other reporters

The immediate consumer is search_cluster.server.state, but the exposure it fixes is shared by every cluster health reporter that reads the search backend. The health framework gives each reporter a 5s wall-clock budget and can only abandon a result, not kill the worker. Its SPI therefore requires each reporter to bound its own backend call. MongoDB-backed reporters already satisfy this using the driver's withTimeout, which bounds the whole operation including retries. The search client has no equivalent, so until now search-backed reporters had no way to comply.

How Tested

Automated: unit tests

Manual: See the linked enterprise PR for description of manual test coverage

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactoring (non-breaking change)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have requested a documentation update.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.

The existing health() cannot be bounded by configuration alone. Its connect
(10s) and socket (60s) timeouts apply per host, and the REST client works
through every configured node inside a single call, so its worst case is a
multiple of both. A caller that must return within a budget of its own -- a
health check with an evaluation deadline -- has no way to get one today.

Add an overload that issues the request asynchronously and cancels it at the
deadline, which bounds wall-clock and stops the client continuing through its
remaining hosts. OS2 and ES7 use PlainActionFuture with the *Async client
methods, the shape cancellableMsearch already uses in both; OS3 routes through
the client's existing executeWithClientTimeout. Each also pulls the request's
own cluster-manager timeout down to the same bound, since it defaults to 30s
and a cluster with no elected manager would otherwise sit there server-side.

A timeout is reported the way an unreachable cluster already is, as an empty
Optional: to a caller on a deadline, "did not answer in time" and "did not
answer" are the same fact. OS3's bounded variant additionally catches the
runtime OpenSearchException an error response produces, which its IOException-
only handler lets escape.

Purely additive. The five existing health() call sites are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
patrickmann and others added 2 commits August 5, 2026 08:16
CompletableFuture.get(timeout) clears the interrupt flag when it throws
InterruptedException, and the blanket catch (Throwable) wrapped and rethrew it
without restoring the flag -- so an interrupt delivered while a bounded call
(e.g. the new clusterHealth(Duration)) was waiting was silently lost, defeating
cooperative cancellation on the pooled worker. Restore it, mirroring the ES7/OS2
bounded health adapters.

Also correct the OS3 bounded-health test comment: the mocked transport delivers
the stubbed 500 as an IOException, so the test pins only that an error response
folds into empty, not the runtime OpenSearchException path (which the mock cannot
produce) nor the deadline firing (which needs a delaying transport).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the conflict in ClusterAdapterOS.java: master #26836 added
countOfClusterManagerEligibleNodes() at the same insertion point where this
branch added the bounded clusterHealth(Duration). The two methods are
orthogonal; kept both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@patrickmann
patrickmann marked this pull request as ready for review August 5, 2026 07:09
@patrickmann
patrickmann requested a review from a team August 5, 2026 07:09
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