Skip to content

feat(pd): add quorum-aware /v1/ready endpoint and raft gauges - #3185

Open
bitflicker64 wants to merge 4 commits into
apache:masterfrom
bitflicker64:fix/pd-ready-endpoint
Open

feat(pd): add quorum-aware /v1/ready endpoint and raft gauges#3185
bitflicker64 wants to merge 4 commits into
apache:masterfrom
bitflicker64:fix/pd-ready-endpoint

Conversation

@bitflicker64

@bitflicker64 bitflicker64 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Purpose of the PR

Main Changes

  • RaftEngine: new hasLeader(), isReady(), getNodeState() and getAlivePeerCount(); isLeader() and getLeader() are now null-safe before the raft node starts.
  • StoreAPI: new unauthenticated GET /v1/ready. Returns 200 with {"ready":true,"state":"STATE_LEADER","isLeader":true} while the raft node is active and sees a leader, 503 with "ready":false otherwise. The body comes from one RaftEngine.getRaftStatus() snapshot and carries no cluster addresses. Added to the auth interceptor exclusion list next to /v1/health.
  • PDMetrics: three gauges for alerting on quorum loss: hg_raft_leader, hg_raft_has_leader, hg_raft_alive_peers (leader only, NaN elsewhere).
  • Docs for PD, Store and docker updated to explain liveness vs readiness. The compose healthchecks deliberately stay on /v1/health: these files run published images, and PD's auth interceptor answers 200 on any path it does not exclude, so a status-only probe reads a PD without the endpoint as ready. The docker README records what switching them over needs, a body match on "ready":true and an image that carries the endpoint.

Why "sees a leader" is the right local signal: jraft resets a follower's leader id once heartbeats stop arriving inside the election timeout, and a leader steps down when it cannot reach a quorum. So a non-null leader id means this node is inside a quorum from its own point of view, which is what a readiness probe needs. This matches the behaviour measured in the issue, where the survivor logged Raft lost leader within a second of the fault.

PD /v1/health vs /v1/ready, today and with this PR

Verify the Changes

  • New RaftEngineReadinessTest (added to PDCoreSuiteTest) covers: no raft node, leader, follower with leader, follower without leader, empty leader id, candidate, transferring, inactive states, and the leadership-loss race in getAlivePeerCount().
  • RestApiTest (runs against the live CI PD) now checks that /v1/health answers 200 with an empty body, that /v1/ready reports ready=true and STATE_LEADER on the single-node PD without disclosing an address, and that the three gauges are exported with the expected values.
  • test-start-hugegraph-pd.sh waits for /v1/ready to return 200 with ready=true after the health endpoint responds.
  • Locally on JDK 11: unit tests 10/10, pd-rest-test 16/16 and test-start-hugegraph-pd.sh 13/13 against a source-built PD. Against a live PD, /v1/ready answers 200 {"ready":true,...} as leader and 503 {"ready":false,...} with two unreachable peers, while /v1/health stays 200 throughout and the gauges move 1/1/1 to 0/0/NaN.

Does this PR potentially affect the following parts?

  • Nope
  • Dependencies (add/update license info)
  • Modify configurations
  • The public API
  • Other affects (typed here)

Notes for reviewers:

  • Kubernetes users should keep liveness probes on /v1/health and point readiness probes at /v1/ready. Using /v1/ready as a liveness probe would restart a PD that merely lost its leader.
  • The Store's own /v1/health is unchanged; this PR only covers PD.
  • A probe on /v1/ready must match the body, not just the status code. RestAuthentication.preHandle rejects by writing an error envelope without calling setStatus, so any non-excluded path answers 200 with {"status":-1,"error":"Unauthorized!"}. Fixing that root cause is out of scope here.

Documentation Status

  • Doc - Updated

/v1/health answers 200 as soon as the Spring listener is up and never
consults the raft state, so a PD that has lost its leader keeps reporting
healthy to every consumer that gates on it (compose healthchecks, the
Store's wait for PD, Kubernetes probes, wait-storage.sh).

Keep /v1/health as pure liveness and add an unauthenticated /v1/ready
that answers 200 only while the raft node is active and sees a leader,
and 503 otherwise. A follower drops its leader id once heartbeats stop
inside the election timeout and a leader steps down when it cannot reach
a quorum, so "sees a leader" is the local view of being inside a quorum.

Export three gauges next to hg_up so operators can alert on quorum loss:
hg_raft_leader (1 on the leader), hg_raft_has_leader (1 while a leader
is known) and hg_raft_alive_peers (peers the leader heard from inside
the election timeout, NaN on non-leaders).

Point the compose PD healthchecks at /v1/ready so Stores are no longer
released against a leaderless PD, and document both endpoints. The PD
startup CI test now also waits for /v1/ready on the live single-node PD,
and the REST suite checks the endpoint and the gauges against it.

Fixes apache#3183
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 37.78%. Comparing base (98477f0) to head (ffa13f9).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
.../pd/rest/interceptor/AuthenticationConfigurer.java 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3185   +/-   ##
=========================================
  Coverage     37.78%   37.78%           
- Complexity     6556     6563    +7     
=========================================
  Files           800      800           
  Lines         68929    68960   +31     
  Branches       9157     9166    +9     
=========================================
+ Hits          26046    26059   +13     
- Misses        39824    39836   +12     
- Partials       3059     3065    +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Blocking: no. Summary: The liveness and readiness split is the right fix for #3183, the jraft assumptions behind it hold, and the new unit tests pass locally. Four minor notes: one on field visibility, two on comment and doc accuracy, one on image-version compatibility for the compose healthcheck change. Evidence: ran mvn -o -pl hugegraph-pd/hg-pd-test -am -Dtest=RaftEngineReadinessTest test on JDK 11 (9/9 pass) after building hg-pd-core and hg-pd-service; checked jraft 1.3.13 directly, where State.isActive() is ordinal() < STATE_ERROR, NodeImpl.listAlivePeers() throws IllegalStateException off-leader under a read lock, and getLeaderId() already maps an empty peer to null; confirmed MetricsConfig.metricsCommonTags adds hg="pd", so the hg_raft_*{ assertions in RestApiTest will match the Prometheus rendering. CI at 4dd7e71 was still running, with the pd, store and hstore integration jobs incomplete, so the live-PD assertions are unverified here.

Comment thread hugegraph-pd/docs/api-reference.md Outdated
Comment thread docker/README.md Outdated
Make RaftEngine.raftNode volatile so /v1/ready and the hg_raft_* gauges,
which read it from request and scrape threads, do not rely on the
@PostConstruct ordering for safe publication, and let isReady() reuse
the node it already snapshotted instead of re-reading the field.

Drop the wait-storage.sh mention from the /v1/ready javadoc: that script
polls /v1/stores and Stores register over gRPC, so the compose
healthcheck and Kubernetes probes are the real consumers.

Move the raft gauge table below the existing /actuator/metrics example
so the example still reads as that command's response, and note that
both quorum-loss expressions are briefly true during a normal election
and need a for: clause longer than the election timeout.

State in the docker README that /v1/ready first ships in 1.8.0, since an
older HUGEGRAPH_VERSION would leave the PD healthcheck failing and the
Stores never starting, and drop a doubled blank line.
bitflicker64 added a commit to bitflicker64/hugegraph that referenced this pull request Sep 2, 2026
…ment health vs ready

PD's /v1/health answers 200 as soon as the REST listener is up and never
consults raft, so every PD and Store probe and the Store init container's
PD wait count listeners, not quorum members (apache#3183). The
fix, apache#3185, adds /v1/ready from 1.8.0.

- pd.readinessPath and store.waitPath, both defaulting to /v1/health, so the
  switch to /v1/ready is a values change made with the 1.8.0 pin; the
  schema rejects paths without a leading slash
- README: Limitations entries for the liveness-only health endpoint and for
  the 45 second discovery lease (measured 30 to 35 seconds); the Store wait
  is described as a PD wait rather than a quorum wait; the Server now
  registers its Pod IP, not the Service URL
- NOTES and the init container messages no longer claim a quorum
- tests: pd_readiness_path_test.yaml, five cases
bitflicker64 added a commit to bitflicker64/hugegraph that referenced this pull request Sep 2, 2026
Points at apache#3185 and says the defaults flip with the 1.8.0
image pin, so the change is not lost once that PR merges.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Blocking: no. Summary: The endpoint, the raft accessors and the gauges are correct and well covered, and the jraft assumptions behind them hold. Two things to fix before merge: the paragraph added at docker/README.md:210-212 states a failure mode that this PR's own CI disproves, and the compose healthchecks it describes do not actually gate on readiness, because PD answers an unauthenticated request with HTTP 200 and an error body. The pre-PR /v1/health probe had the same property, so this is a missed improvement rather than a regression. Evidence: RestAuthentication.preHandle:61-66 writes the error body and returns false without response.setStatus(...); in CI run 33642694186, job build-server (rocksdb, 11), the hstore smoke pulled Docker Hub hugegraph/pd:latest (git grep '/v1/ready' origin/master -- hugegraph-pd is empty) and logged Container ...-pd-1 Healthy 11 seconds after start; PDCoreSuiteTest (101 run, 2 skipped) and PDRestSuiteTest (16 run) pass at 5bd1b96.

Comment thread docker/docker-compose-hstore.yml Outdated
Comment thread docker/README.md Outdated
PD's auth interceptor rejects a request by writing an error envelope
without setting a status, so every path it does not exclude, including a
path that does not exist, answers 200. A healthcheck that only inspects
the status code therefore reads a PD too old to carry /v1/ready as ready,
which is the same "healthy without a quorum" shape this PR set out to
fix. The compose files run published images, so revert their PD
healthchecks and the manual verification calls to /v1/health and document
what switching them over needs: a body match on "ready":true, and an
image that carries the endpoint.

Build the /v1/ready body from one RaftEngine.getRaftStatus() snapshot,
taken from a single Node reference and a single getLeaderId() read, so a
step-down midway cannot report a ready node that knows no leader.

Drop the leader's raft address from the body. The endpoint is
unauthenticated and the address was the one new disclosure; leadership
itself is already published by the hg_raft_leader gauge, and the address
stays on the authenticated /v1/members.

Call the window in the hg_raft_alive_peers description what jraft
measures, the leader lease timeout, which it derives as 90% of the
election timeout by default, rather than the election timeout.

Assert the empty body in testHealthNeedsNoAuth, since a 200 alone cannot
tell an anonymous path from a rejected one.

@imbajin imbajin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: no. Summary: The readiness implementation is correct, but the deployment guide overstates which compose probe is active. Evidence: JDK 11 RaftEngineReadinessTest passed 10/10; the current compose files still use /v1/health, and the Codecov patch failure is non-blocking.

Comment thread hugegraph-store/docs/deployment-guide.md Outdated
The startup ordering list said PD healthchecks probe /v1/ready, but
c8adc85 put both compose files back on /v1/health and this line was
missed, so the guide described a quorum gate that does not exist.

Name /v1/health, say it is liveness only, and point at docker/README.md
for what pointing the healthchecks at /v1/ready would require.
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.

[Bug] PD /v1/health reports healthy without a raft quorum; add a quorum-aware readiness signal

2 participants