Skip to content

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

Merged
imbajin merged 25 commits into
apache:masterfrom
bitflicker64:fix/pd-ready-endpoint
Sep 8, 2026
Merged

feat(pd): add quorum-aware /v1/ready endpoint and raft gauges#3185
imbajin merged 25 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(), getRaftStatus() and getAlivePeerCount(), plus the nested RaftStatus view (isReady(), getState(), isLocalLeader()) the endpoint and the gauges both read. hasLeader() is getRaftStatus().isReady(), so the gauge cannot drift from the endpoint. 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), twelve cases: not ready before the raft node starts, a started node with no callback yet, leader ready, follower with a leader ready, a follower losing its leader, a leader stepping down, error and shutdown after leadership, the shutdown state surviving the raft node being dropped, the probe never touching the raft node, the leadership-loss race in getAlivePeerCount(), the count on a leader still skipping the node, and the count falling back to -1 once a leader errors or shuts down.
  • New StoreAPIReadyTest in hg-pd-service drives the endpoint itself: 503 before the raft node starts, 503 once the leader is lost, and 200 with STATE_LEADER on a leader, all without touching the raft node.
  • 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 at 4cca88a: mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-core-test passes 104/104 (2 skipped), and test-start-hugegraph-pd.sh passes 13/13 against a PD built from this tree. pd-rest-test and pd-client-test run against a live PD in CI. Earlier on the branch, against a live PD, /v1/ready answered 200 {"ready":true,...} as leader and 503 {"ready":false,...} with two unreachable peers, while /v1/health stayed 200 throughout and the gauges moved 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 (bed2e45) to head (0daffc6).

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     6560     6563    +3     
=========================================
  Files           800      800           
  Lines         68960    68960           
  Branches       9166     9166           
=========================================
+ Hits          26054    26058    +4     
+ Misses        39839    39836    -3     
+ Partials       3067     3066    -1     

☔ 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.

@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, and at this head the endpoint, the raft accessors and the three gauges all check out; three minor notes remain, all about wording rather than behaviour. Evidence: jraft-core 1.3.13 State.isActive() is ordinal() < STATE_ERROR.ordinal() over LEADER, TRANSFERRING, CANDIDATE, FOLLOWER, ERROR, ..., so a candidate counts as active; NodeImpl.getAliveNodes compares against leaderLeaseTimeoutMs and calls no checkReplicator, so hg_raft_alive_peers is side-effect free on every scrape; hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml:61 has a single-peer peers-list, so the new wait_for_pd_ready gate in test-start-hugegraph-pd.sh is reachable on a one-node dist; PDService redirects non-leader gRPC calls to the leader (putLicense at PDService.java:1376 is the one exception), so a follower that sees a leader really can serve. CI on ffa13f9 is green except codecov/patch.

Comment thread docker/README.md
Comment thread hugegraph-pd/docs/api-reference.md Outdated
State.isActive() is ordinal() < STATE_ERROR over LEADER, TRANSFERRING,
CANDIDATE, FOLLOWER, so a candidate is active too and the isReady()
javadoc was a state short. Name the set jraft actually uses.

Say what the candidate test exercises. jraft clears the leader id before
starting an election, so testCandidateWithoutLeaderIsNotReady passes on
the missing leader rather than on the state, and a second case records
that a candidate does count as active. Mark the empty-peer test as
guarding the Node contract, since NodeImpl maps an empty peer to null.

Date the interceptor behaviour instead of asserting it as a property of
PD. As of 1.7.0 a refusal carries 200 and an error envelope, which is
what makes a status-only probe read an older PD as ready, but the body
match holds whichever status a refusal carries. Same wording in the
docker README and in testHealthNeedsNoAuth.

Note that the HEALTHCHECK baked into hugegraph-pd/Dockerfile is on
liveness as well. Both compose files override it, so it governs docker
run and anything else inheriting the image probe.
RaftEngine.isReady() had no caller outside its own tests: the endpoint
reads getRaftStatus() and the gauges read isLeader() and hasLeader().
Remove it and keep its note on the active-state set where isActive() is
actually called.

testStatusNeverReportsReadyWithoutALeader only repeated the two follower
shapes the tests either side of it already cover, so drop it and let the
rest assert through the snapshot, which is the path production takes.

Reduce wait_for_pd_ready to the gate the docs recommend, curl -f piped
into grep. -f rejects the 503 and the body match rejects a 200 that is an
auth envelope, so the hand-rolled status parsing bought nothing.
The exclusion list was the one line this change left uncovered, and it
carries a contract worth holding: if /v1/ready slips back behind the
interceptor, PD answers a probe with 200 and an auth envelope instead of
a readiness answer, so every healthcheck matching on the body holds
forever while the status still looks healthy.

Drive AuthenticationConfigurer with a real InterceptorRegistry and assert
through MappedInterceptor.matches(), so the test states the behaviour,
that these paths are not intercepted, rather than the literal patterns.
/v1/members and friends stay intercepted in the same test.

Verified by mutation: dropping /v1/ready from the list fails
testProbeEndpointsAreAnonymous.
The pd job runs mvn clean package between the core tests and the codecov
upload, which wipes the exec file the core run appended to, so only the
client and rest profiles reach the report. Move the check to
PDRestSuiteTest, where it also sits closer to the REST layer it covers.
This reverts commit 27f7009. Its reason was wrong: I read the pd job
from a stale checkout, where mvn clean package sat between the core
tests and the upload. On this branch Package runs first, then the four
test profiles append to one exec, and the aggregate report is generated
after the rest test, so core-test coverage reaches Codecov either way.

With that settled the core suite is the better home. The check is a pure
unit test, and the rest profile needs a live PD for the rest of its
suite.

@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 readiness split is well-reasoned and the jraft usage checks out against the pinned jraft-core 1.3.13 (getNodeState()/State.isActive()/listAlivePeers() all behave as the javadoc claims, and getAliveNodes() really does include the node itself); three minor polish items only, none of which should hold up merge. Evidence: git diff 98477f0f5 refs/remotes/pr/3185 for the exact-head diff; javap on com.alipay.sofa.jraft.Node and com.alipay.sofa.jraft.core.State plus NodeImpl.java from the 1.3.13 sources jar (listAlivePeers() throws IllegalStateException off-leader at L2977, getLeaderId() already maps an empty peer to null at L2487, getAliveNodes() adds serverId at L2266); git show 98477f0f5:.../RestAuthentication.java confirms preHandle writes the error envelope without setStatus, so the docs' "match the body, not the status" guidance is correct; MetricsConfig.metricsCommonTags() registers commonTags("hg", "pd"), so the new gauges render with the {...} block the RestApiTest assertions expect; callers of isLeader()/getLeader() audited repo-wide for the new null-safety and none regress. Not verified: no build or test run against this head, and the effective Spring version comes from a parent BOM, so trailing-slash interceptor matching on /v1/ready/ was left out of these comments.

Comment thread hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh Outdated

@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: yes. Summary: The new interceptor test does not compile at this head, so the PD test and server build jobs cannot pass. Evidence: GitHub Actions run 33839755062 (pd and hstore) and run 33839755249 (both macOS server jobs) report cannot find symbol for AuthenticationConfigurer and RestAuthentication at AuthenticationConfigurerTest.java:53-54; both classes are defined in hg-pd-service.

hg-pd-service is repackaged by spring-boot-maven-plugin, so the artifact
a full mvn install leaves behind is an executable jar with classes under
BOOT-INF/classes, invisible to javac. A mvn test -am reactor compiles
hg-pd-test against target/classes instead, which is why the test passed
in the pd job and locally while build-commons and both macOS server jobs
failed with cannot-find-symbol.

Moving the test into hg-pd-service would not help: every CI package step
runs -Dmaven.test.skip=true and the pd profiles only execute suites in
hg-pd-test, so it would never run. The anonymity of /v1/health and
/v1/ready stays pinned by the live REST tests, which asserted it before
the unit test existed.
Derive the snapshot's leader flag from the state read one line above
instead of a third locked node call. jraft's isLeader(true) is exactly
state == STATE_LEADER, so the value is unchanged while the fields can no
longer contradict each other, which is what the javadoc promised.

Name the gauges hg.raft.has.leader and hg.raft.alive.peers. Micrometer
renders both to the same Prometheus names as before, but dot-separated
segments keep other registries from mixing separators.

Capture the readiness body in wait_for_pd_ready instead of piping into
grep -q: under the script's pipefail a SIGPIPE-killed curl could misread
a ready PD, and wait_for_pd already uses the capture shape.
bitflicker64 added a commit to hugegraph/hugegraph that referenced this pull request Sep 5, 2026
PD gains the quorum-aware /v1/ready endpoint, which answers 503 without a
raft leader, and the hg_raft_* gauges. The endpoint sits outside the auth
interceptor. The pull request is open against master; this branch carries it
so the helm-dev images can be tested with pd.readinessPath and
store.waitPath set to /v1/ready (apache#3183).
bitflicker64 added a commit to hugegraph/hugegraph that referenced this pull request Sep 5, 2026
PD REST now checks the password against auth.secret-key and answers 401 on
refusal (apache#3188). The PD image requires HG_PD_AUTH_SECRET_KEY,
wait-storage.sh sends PD_AUTH_PASSWORD, and Hubble reads
operations.pd.password.

Two conflicts with the pull requests merged before it, both resolved as the
union: the interceptor exclusion list keeps /v1/ready from apache#3185 alongside
the /actuator/** widening from apache#3189, and test-compose.sh runs the startup
timeout asserts from apache#3187 followed by the Hubble helper check from apache#3189.
render_with_timeout from apache#3187 additionally passes HG_PD_AUTH_SECRET_KEY,
which the Compose files require since apache#3189; without it the render step
would fail on the two HStore topologies.

The chart does not yet supply the PD secret, so PD Pods from an image built
at this revision will not start under the chart until that wiring lands.
@bitflicker64
bitflicker64 requested a review from imbajin September 5, 2026 06:57
bitflicker64 added a commit to bitflicker64/hugegraph that referenced this pull request Sep 5, 2026
PD images from 1.8.0 (apache#3189) check the Basic-auth password
of every management call against auth.secret-key and refuse to start without
one. The chart now keeps that value in a kept release-pd-auth Secret, or in
pd.auth.existingSecret, and hands it to the three readers: PD as
HG_PD_AUTH_SECRET_KEY, the Server storage wait as PD_AUTH_PASSWORD, and
Hubble as operations.pd.password written into its properties file by the
existing wrapper. A checksum/pd-auth annotation on the three Pod templates
rolls them when the Secret changes; the Server annotations block is now
rendered unconditionally for it. Priority and lookup semantics mirror
server.auth.token. Older images ignore the password, so the wiring is
harmless on the images the draft currently tracks.

The values schema requires one of existingSecret, value or autoGenerate and
refuses newlines, carriage returns and backslashes in an inline value, since
it lands in a Java properties file; the template guard repeats the first
rule for values that bypass the schema. The three chart-managed variables
join the reserved extraEnv lists.

README: Chart Details bullet, four parameter rows, Disaster Recovery calls
carry the secret, and the Limitations bullet separates the 1.7.0 behaviour
from 1.8.0. NOTES prints how to read the secret. New suite
pd_auth_secret_test.yaml, 9 tests; 58 in total. Lint on three presets;
renders 16 objects by default and 19 with Hubble.

Measured on a kind cluster with images built from master plus apache#3185, apache#3187
and apache#3189: the Secret is created, PD starts with the variable, the Server
storage wait passes with the credential, and Hubble lists all nine nodes.
@bitflicker64

Copy link
Copy Markdown
Contributor Author

Tested end to end on Kubernetes on 2026-09-05, with this branch merged into the hugegraph/hugegraph testing tree.

Build under test. Tag helm-dev-20260905, commit 6ec19838: apache master 36811483 plus this PR at 70744406, #3187 and #3189. PD, Store and Server images built from that tree with docker buildx bake -f docker/bake.hcl pd store server-hstore (linux/amd64), labelled org.opencontainers.image.revision=6ec1983889f58a52995b27a4e1ea89bd00932396, and the label read back from every running pod. kind v0.33.0, Kubernetes 1.37.0, 1 control-plane + 3 workers, the #3132 Helm chart with pd.readinessPath=/v1/ready and store.waitPath=/v1/ready, 3 PD + 3 Store + 3 Server + Hubble.

What held. Deleting two of three PDs with kubectl delete pod, the survivor sampled every 2 s through a port-forward, two runs:

  • /v1/ready on the survivor: 503 with hg_raft_has_leader 0 2.6 s after the delete, back to 200 with hg_raft_has_leader 1 once the replacements rejoined, 12 to 20 s later.
  • /v1/health on the same pod: 200 on all 313 samples of run one and all 90 of run two, so liveness and readiness now disagree exactly when they should.
  • A Store deleted inside the window sat in its wait-for-pd init container (Waiting for 2 PD peers to answer /v1/ready) from T0+7 s and passed at T0+23 s, the same second the first 200 came back. Schema writes through the Server hung inside the window and returned 202 after it.
  • The gauges behaved as documented: hg_raft_alive_peers read 3.0 while the sampled node was leader and NaN once leadership moved to a replacement.

One thing worth a look before merge: the handler stalls during the election. With curl -m 10 -w '%{time_total}', the first /v1/ready request after the delete took 9.79 s to return its 503; every 200 before and after answered in under 5 ms. In the first run, with a 2 s client timeout, every sample inside the leaderless window except the first came back as a timeout rather than a 503. A kubelet probe with timeoutSeconds: 5 fails either way, so the readiness contract is met, but the endpoint promises a prompt 503, not a stall. Reading the path: StoreAPI.checkReady calls RaftEngine.getRaftStatus, which reads node.getNodeState() and node.getLeaderId(), both under NodeImpl's lock; while the node runs its election it holds the write lock and opens connections to two peers that no longer exist. I have not profiled it, so treat the lock as the lead and the 9.79 s as the measurement. One way out is to keep a volatile copy of the last known state and leader, updated from the state machine callbacks (onLeaderStart, onLeaderStop, onStartFollowing, onStopFollowing), and serve the probe from that, so it never waits on the node lock.

The sample logs and scripts are kept with the campaign notes; I can attach them here if useful.

@bitflicker64

Copy link
Copy Markdown
Contributor Author

Logs and scripts from the two runs above, hosted on my fork (branch logs/pr-3185, index):

  • d3q-samples.log and d3q-b-samples.log: one line per 2 s on the surviving PD: epoch, /v1/ready, /v1/health, the hg_raft_* gauges, the Pod Ready condition. Run 2 appends time_total to the ready column as code/seconds.
  • d3q.log and d3q-b.log: script output (pre-fault uids and IPs, T0, the Store gate, post-recovery checks, blocked-line counts).
  • d3q.sh and d3q-b.sh: the scenario scripts, oracle declared before the fault.

The stall, from run 2 (ready=<code>/<seconds>):

1788593443.186162738 ready=200/0.003056 health=200 hg_raft_has_leader=1.0 podReady=True
1788593445.237568308 ready=200/0.002668 health=200 hg_raft_has_leader=1.0 podReady=True
1788593447.290482850 ready=200/0.003203 health=200 hg_raft_has_leader=1.0 podReady=True
1788593449.335970063 ready=503/9.792368 health=200 hg_raft_has_leader=1.0 podReady=True
1788593461.199044573 ready=200/0.002740 health=200 hg_raft_has_leader=1.0 podReady=True
1788593463.250381323 ready=200/0.002739 health=200 hg_raft_has_leader=1.0 podReady=True

T0 was 1788593447, the third line's timestamp. The fourth line's request went out at about T0+2.3 s and got its 503 at about T0+12.1 s, 9.79 s later; the gauge on that same line was read after the stall, once the replacements were back, which is why it already shows has_leader 1.0. The fifth line answered 200 in 2.7 ms. Run 1 shows the same window as ready=000 (2 s timeouts) from T0+4.6 s to T0+22.8 s after one prompt 503 at T0+2.6 s with has_leader 0.0.

@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 callback-served readiness path is sound and its jraft assumptions hold up, but getAlivePeerCount() still takes the raft node's read lock, so a metrics scrape can block on the very lock /v1/ready was restructured to avoid. Evidence: jraft-core 1.3.13 sources resolved for hg-pd-core (NodeImpl.listAlivePeers, NodeImpl.stepDown, NodeImpl.preVote, AbstractClientService.connect) read against the head diff.

@imbajin
imbajin requested a lite review from Copilot September 7, 2026 10:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Adds a quorum-aware readiness signal for PD and corresponding raft membership gauges, clarifying liveness vs readiness for operators and automation.

Changes:

  • Introduces unauthenticated GET /v1/ready that returns 200 only when PD sees a raft leader (otherwise 503)
  • Exports raft quorum-related gauges (hg_raft_leader, hg_raft_has_leader, hg_raft_alive_peers) for alerting
  • Updates tests, scripts, and docs to use/readiness semantics and avoid false-ready probes

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
hugegraph-store/docs/deployment-guide.md Documents PD liveness (/v1/health) vs readiness (/v1/ready) for deployment gating
hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh Adds readiness wait loop that checks /v1/ready content before proceeding
hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/RestApiTest.java Adds REST tests for anonymous health/ready endpoints and new metrics gauges
hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java Adds unit tests for raft-derived readiness behavior and edge cases
hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java Includes new readiness test in the PD test suite
hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/interceptor/AuthenticationConfigurer.java Excludes /v1/ready from auth interceptor like /v1/health
hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java Implements GET /ready endpoint response and status selection
hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/PDMetrics.java Registers new raft membership gauges
hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftStateMachine.java Adds lock-free probe view updated by raft callbacks
hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java Adds quorum-aware status snapshot, leader visibility helpers, and null-safety
hugegraph-pd/docs/api-reference.md Documents health vs ready endpoints and new metrics
hugegraph-pd/README.md Mentions new liveness/readiness endpoints
docker/README.md Explains why compose healthchecks remain on /v1/health and how to safely switch to readiness

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh Outdated
listAlivePeers takes the jraft node read lock before it checks for
leadership, so a scrape that called it waited out whoever held the write
lock. stepDown holds that lock while it writes raft metadata on a term
bump, and preVote holds it across a reconnect to every peer, bounded only
by raft.rpc-timeout.

The gauge now reads a volatile count that a one second refresher
publishes on its own daemon thread, and reports -1, so NaN, until the
first refresh. jraft walks the same peer set in its own step down timer
every half election timeout, so the poll costs nothing next to what the
node already does. Drop the javadoc claim that the call only runs on a
settled leader, which the lock order cannot support.
Java assert is a no-op unless the jvm runs with -ea, so the health, ready
and gauge checks added on this branch could pass without ever running.
Convert them, keeping the messages they already carried.

The gauge patterns now accept a sample line with no tag block, which
micrometer emits for a meter without tags, and anchor on the start of a
line so a HELP or TYPE line cannot satisfy them.
The rest suite only reaches the ready path, because the PD it talks to is
a single node group that is always its own leader. Nothing pinned the 503
half of the mapping, so an always-200 regression would have shipped.

Cover checkReady() over a node that has not started raft and over a
follower whose leader went away, plus the leader case so the mapping
cannot be inverted either. The test sits in hg-pd-service: the other CI
jobs compile hg-pd-test against the repackaged service jar, where the
class is not visible.
The probe grepped for the literal "ready":true, so it would stop matching
if the serializer ever emitted a space after the colon or pretty-printed
the body. Match the key, optional whitespace and the value instead.
scheduleWithFixedDelay cancels every later run if the task throws, and the
catch only covered Exception, so an Error would have stopped the refresher
for good while the gauge kept serving its last value with no NaN and no
log line. Widen it to Throwable, which is what the comment already claimed.

shutDown also reset the count before an in-flight refresh could finish, so
a refresh already inside listAlivePeers could publish a positive count
afterwards. Wait briefly for the executor to quiesce first.
Brings in the master merge already made on the hugegraph/hugegraph mirror,
so the fork and the mirror share one history again. Master now carries
apache#3187; it does not touch anything this branch changes.

@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 readiness design holds up against the jraft 1.3.13 sources actually on the classpath — State.isActive() excludes STATE_UNINITIALIZED, NodeImpl.handleElectionTimeout resets the leader id while still STATE_FOLLOWER so onStopFollowing really does fire on heartbeat loss, stepDown fires onLeaderStop before resetting, and all six hooked callbacks are delivered on FSMCallerImpl's single disruptor consumer, so onStopFollowing's read-modify-write is single-threaded as documented. Three minor points only: hasLeader() drops the active-state guard getRaftStatus() applies, the awaitTermination guard in shutDown() cannot prevent what its comment claims, and the PR description no longer matches this head. Evidence: git diff bed2e457..63a26900 in the configured checkout (15 files, +799/-6, matching gh api repos/apache/hugegraph/pulls/3185/files); read com/alipay/sofa/jraft/core/State.java and NodeImpl.java:615-644,1195-1206,1268-1290 from jraft-core-1.3.13-sources.jar; PDConfig.java:153 (raft.rpc-timeout default 10000 ms) against RaftEngine.init's setRpcConnectTimeoutMs; hugegraph-pd/Dockerfile:68-69, docker/docker-compose-hstore.yml:47 and docker/docker-compose-3pd-3store-3server.yml:40 to check the docs' probe claims; install-dist/scripts/dependency/regenerate_known_dependencies.sh (-DincludeScope=runtime) for the new test-scope junit; and the CI job log for this head (run 34155085574, job pd), which shows StoreAPIReadyTest 3/0/0 under surefire (default-test) @ hg-pd-service, PDCoreSuiteTest 102/0/0, PDRestSuiteTest 16/0/0 and PASS PD readiness endpoint reports a raft leader. Not independently built or run locally (only JDK 17 available, project targets 11), and no 3-PD fault injection was reproduced.

@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: Fresh pass at 63a2690, the same head the last review covered; no new findings, so this adds applyable suggestions for that review's two code items. The third item, the stale PR description, has no code anchor and still needs a body edit before the squash merge. Evidence: the hasLeader() derivation was traced through all ten RaftEngineReadinessTest cases, including testProbeNeverTouchesTheRaftNode (a field null check is not a mock interaction); micrometer-core 1.7.12 sources confirm Gauge.builder(String, Supplier) ends in .strongReference(true), so the supplier-fed gauges cannot be collected into a permanent NaN. Not verified by a local build.

hasLeader() omitted the active-state guard getRaftStatus() applies, so
hg_raft_has_leader and GET /v1/ready agreed only by accident of which
callback writes what. Derive the gauge from getRaftStatus().isReady() so
the two cannot drift.

getRaftStatus() forced STATE_UNINITIALIZED whenever the raft node was
null, which masked the shutdown and error states the callbacks announce
and contradicted its own doc. The node check is unnecessary: the probe
view starts uninitialized, and shutDown() drops the node only after the
shutdown callback has run, so neither case reads as ready.

onError() and onShutdown() left leaderTerm positive when the node had
been leader, so the alive-peer refresher kept calling listAlivePeers()
on a broken node and hg_raft_alive_peers reported a live count instead
of NaN. Clear the term in both.

The wait in shutDown() cannot prevent a late publish: a refresh parked
in listAlivePeers() sits on a lock acquire the interrupt does not break,
for up to the raft rpc connect timeout per unreachable peer. Keep the
wait, say what it actually does, and log when the refresher outlives it.
State the per-node result of /v1/ready in docker/README.md: in a
three-PD group a partitioned node keeps answering 503 while the two that
can reach each other elect a leader, rather than all three turning ready
together.

Note in the store deployment guide that /v1/ready ships after 1.7.0, the
version its Docker examples pin, so the readiness check there needs a
newer tag or images built from source.

Shorten the hg_raft_alive_peers row in the PD API reference and move the
leader lease timeout detail into a paragraph after the table.

Give the readiness curl in test-start-hugegraph-pd.sh a connect and a
total timeout. Without them a PD that accepts the connection and then
stops answering parks the loop, elapsed never advances, and STARTUP_WAIT
stops bounding the CI wait.
The table cell still carried the full lease-timeout explanation. Move
the rest of it into the paragraph under the table so the row reads at
a glance.
The per-round counter added 2s per iteration while an attempt could cost
up to --max-time plus the sleep, so a PD that accepted the connection and
then stopped answering ran the loop well past STARTUP_WAIT. Measured
against a socket that accepts and never replies: 70s elapsed for a
STARTUP_WAIT of 20, which is ~210s at the 60 CI uses. A deadline taken
from SECONDS bounds it at 21s.
@imbajin
imbajin merged commit e85b36a into apache:master Sep 8, 2026
23 of 24 checks passed
bitflicker64 added a commit to bitflicker64/hugegraph that referenced this pull request Sep 8, 2026
apache#3185 landed and both branches rewrote the same excludePathPatterns call.
Took the union: /actuator/** from this branch, /v1/ready from apache#3185.
Dropping either side would re-authenticate the readiness endpoint or
reinstate the single-star pattern the docs on this branch now contradict.
bitflicker64 added a commit to hugegraph/hugegraph that referenced this pull request Sep 9, 2026
Brings in the upstream forms of the three fixes this branch had been
carrying as PR-branch merges: apache#3185 (quorum-aware PD /v1/ready and raft
gauges), apache#3187 (configurable Server startup timeout) and apache#3189 (PD REST
credential validation, 401 on refusal).

Every conflicted path, and the two that auto-merged into doubled hunks
(hg-pd-service/pom.xml, travis/start-pd.sh), is resolved to master's
content. Outside helm/ the tree is now identical to master apart from
the chart overlay: the helm-chart-ci workflow, the .tgz excludes in
pom.xml and .dockerignore, and the chart section in README.md.
bitflicker64 added a commit to hugegraph/hugegraph that referenced this pull request Sep 9, 2026
apache#3185, apache#3187 and apache#3189 are merged upstream and due in 1.8.0,
so the chart no longer has to wait for them behind a TODO.

- pd.readinessPath and store.waitPath default to /v1/ready, which answers 503
  until a raft leader exists. PD readiness now means quorum membership and the
  Store's PD wait counts quorum members rather than live listeners. Startup and
  liveness stay on /v1/health so a PD that merely lost its leader is not
  restarted. On a PD image that predates apache#3185 both values have to be set back
  to /v1/health.
- The Server gets HG_SERVER_STARTUP_TIMEOUT_S from the startup probe budget
  (effective failureThreshold * periodSeconds, 450s by default, capped at the
  entrypoint's 86400s). The image default of 120s is shorter than the storage
  wait alone, so the start command used to kill a Server that was still coming
  up. The variable joins the reserved list, because a server.extraEnv duplicate
  would silently decouple the process timeout from the probe.
- Drops the two TODO comments the paths carried, and rewrites the README
  Limitations bullets on PD REST auth and /v1/health vs /v1/ready so the
  current behaviour leads and the older behaviour is the caveat.

Chart 0.1.6, repackaged. lint x3 presets clean, 63 unit tests pass (5 new),
renders 19/17/20 with one /v1/ready path and HG_SERVER_STARTUP_TIMEOUT_S=450.
bitflicker64 added a commit to bitflicker64/hugegraph that referenced this pull request Sep 9, 2026
Picks up apache#3182 (index result filtering) and the three fixes this chart
depends on: apache#3185 (quorum-aware PD /v1/ready and raft gauges), apache#3187
(configurable Server startup timeout) and apache#3189 (PD REST credential
validation, 401 on refusal). No conflicts; the branch touches only
helm/, the chart CI workflow and the README chart section.
bitflicker64 added a commit to bitflicker64/hugegraph that referenced this pull request Sep 9, 2026
apache#3185, apache#3187 and apache#3189 are merged upstream and due in 1.8.0,
so the chart no longer has to wait for them behind a TODO.

- pd.readinessPath and store.waitPath default to /v1/ready, which answers 503
  until a raft leader exists. PD readiness now means quorum membership and the
  Store's PD wait counts quorum members rather than live listeners. Startup and
  liveness stay on /v1/health so a PD that merely lost its leader is not
  restarted. On a PD image that predates apache#3185 both values have to be set back
  to /v1/health.
- The Server gets HG_SERVER_STARTUP_TIMEOUT_S from the startup probe budget
  (effective failureThreshold * periodSeconds, 450s by default, capped at the
  entrypoint's 86400s). The image default of 120s is shorter than the storage
  wait alone, so the start command used to kill a Server that was still coming
  up. The variable joins the reserved list, because a server.extraEnv duplicate
  would silently decouple the process timeout from the probe.
- Drops the two TODO comments the paths carried, and rewrites the README
  Limitations bullets on PD REST auth and /v1/health vs /v1/ready so the
  current behaviour leads and the older behaviour is the caveat.

lint x3 presets clean, 63 unit tests pass (5 new), renders 16/14/16 with one
/v1/ready path and HG_SERVER_STARTUP_TIMEOUT_S=450. Templates, values.schema.json
and tests are byte-identical with the helm-dev testing branch.
bitflicker64 added a commit to bitflicker64/hugegraph that referenced this pull request Sep 10, 2026
Chart 0.1.6 moved pd.readinessPath and store.waitPath to /v1/ready and
updated most of the surrounding prose, but three places were left
describing the old default: templates/NOTES.txt, which is printed to the
operator on every install and every upgrade, and the Configuration rows
for both values, where the Default column still read /v1/health.

The two rows also gave the wrong reason for setting /v1/health on an
older image. They said /v1/ready "exists only on PD images from 1.8.0",
which reads as though an older image would simply reject it. It does
not. On a PD image predating apache#3189 the auth interceptor
writes its error body without setting a status, so every unmapped path
under /v1/ answers 200, and /v1/ready is such a path. The readiness
probe then passes unconditionally and the signal means nothing.

Measured on a PD image built from bed2e45, the parent of the apache#3185
merge, so it carries apache#3187 but neither apache#3185 nor apache#3189: all three PD
pods reported Ready, /v1/ready returned 200 with an Unauthorized body,
and an arbitrary nonexistent path returned the same 200. On merged
master both return 401.

No template behaviour changes. Renders stay 16/14/16 and the 63 unit
tests pass.
bitflicker64 added a commit to hugegraph/hugegraph that referenced this pull request Sep 10, 2026
Chart 0.1.6 moved pd.readinessPath and store.waitPath to /v1/ready and
updated most of the surrounding prose, but three places were left
describing the old default: templates/NOTES.txt, which is printed to the
operator on every install and every upgrade, and the Configuration rows
for both values, where the Default column still read /v1/health.

The two rows also gave the wrong reason for setting /v1/health on an
older image. They said /v1/ready "exists only on PD images from 1.8.0",
which reads as though an older image would simply reject it. It does
not. On a PD image predating apache#3189 the auth interceptor
writes its error body without setting a status, so every unmapped path
under /v1/ answers 200, and /v1/ready is such a path. The readiness
probe then passes unconditionally and the signal means nothing.

Measured on a PD image built from bed2e45, the parent of the apache#3185
merge, so it carries apache#3187 but neither apache#3185 nor apache#3189: all three PD
pods reported Ready, /v1/ready returned 200 with an Unauthorized body,
and an arbitrary nonexistent path returned the same 200. On merged
master both return 401.

Version bumped to 0.1.7 and the packaged archive rebuilt, because 0.1.6
is already tagged and published and its content must not change. The
upgrade note says the step rolls nothing. Renders stay 19/17/20 and the
63 unit tests pass. templates/NOTES.txt stays byte-identical with the
same fix on the apache#3132 branch.
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

3 participants