bitflicker64 commented on code in PR #3185:
URL: https://github.com/apache/hugegraph/pull/3185#discussion_r3918294000


##########
docker/docker-compose-hstore.yml:
##########
@@ -44,7 +44,7 @@ services:
     volumes:
       - pd-data:/hugegraph-pd/pd_data
     healthcheck:
-      test: ["CMD-SHELL", "curl -fsS http://localhost:8620/v1/health 
>/dev/null"]
+      test: ["CMD-SHELL", "curl -fsS http://localhost:8620/v1/ready 
>/dev/null"]

Review Comment:
   ⚠️ This probe cannot tell a ready PD from one that has no `/v1/ready` at 
all, so the gate in front of `store` still checks only liveness.
   
   PD's auth interceptor rejects an unauthenticated request by writing a JSON 
error body and returning `false` without calling `response.setStatus(...)` 
(`hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/interceptor/RestAuthentication.java:61-66`),
 so the response commits with the servlet default 200. On a build that lacks 
the new `/v1/ready` exclusion, `curl -fsS` exits 0 on 
`{"status":-1,"error":"Unauthorized!"}`.
   
   Measured here. `docker/test-compose.sh:270` defaults `HUGEGRAPH_VERSION` to 
`latest` and nothing under `.github/` sets it, so `smoke` pulls Docker Hub 
`hugegraph/pd:latest`, which predates this PR (`git grep '/v1/ready' 
origin/master -- hugegraph-pd` is empty). Run 33642694186, job `build-server 
(rocksdb, 11)`: pd `Started` 14:39:17, `Healthy` 14:39:28. Eleven seconds, 
against an image with no readiness endpoint, so `Compose smoke passed: hstore` 
does not exercise this change.
   
   Not a regression, since the pre-PR `/v1/health` probe was liveness-only too. 
But as written the switch buys nothing.
   
   Requested change: check the payload, the way 
`test-start-hugegraph-pd.sh:123` already does with `[[ "$body" == 
*'"ready":true'* ]]`. For this line:
   
       test: ["CMD-SHELL", "curl -fsS http://localhost:8620/v1/ready | grep -q 
'\"ready\":true'"]
   
   and for `docker-compose-3pd-3store-3server.yml:40`, where the trailing `|| 
exit 1` has to sit after the pipeline:
   
       test: ["CMD-SHELL", "curl -fsS http://localhost:8620/v1/ready | grep -q 
'\"ready\":true' || exit 1"]
   
   Left as plain blocks rather than committable suggestions on purpose: 
applying them reds `docker/test-compose.sh smoke` until an image carrying 
`/v1/ready` is published. Pin the smoke run to source-built images at the same 
time, or keep the compose files on `/v1/health` and land the endpoint, gauges 
and docs first.



##########
hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/RestApiTest.java:
##########
@@ -62,6 +62,51 @@ public void testQueryClusterInfo() throws 
URISyntaxException, IOException, Inter
         assert obj.getInt("status") == 0;
     }
 
+    @Test
+    public void testHealthNeedsNoAuth() throws URISyntaxException, IOException,
+                                             InterruptedException {
+        String url = pdRestAddr + "/v1/health";
+        HttpRequest request = HttpRequest.newBuilder().uri(new 
URI(url)).GET().build();
+        HttpResponse<String> response = client.send(request, 
HttpResponse.BodyHandlers.ofString());
+        assert response.statusCode() == 200;

Review Comment:
   🧹 `statusCode() == 200` cannot tell "no auth required" apart from "auth 
rejected": PD's interceptor answers an unauthenticated request with `200` and 
an error envelope, per the comment on `docker-compose-hstore.yml:47`. This test 
would still pass if `/v1/health` were dropped from `AuthenticationConfigurer`.
   
   `testReadyNeedsNoAuthAndReflectsRaft` is fine, because 
`obj.getBoolean("ready")` would throw on that envelope.
   
   Requested change: assert the body too. `checkHealthy()` returns `""`.
   
   ```suggestion
           assert response.statusCode() == 200;
           assert response.body().isEmpty() : "expected an empty body, got " + 
response.body();
   ```



##########
hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java:
##########
@@ -386,4 +396,30 @@ class StoreStatistics {
     public Serializable checkHealthy() {
         return "";
     }
+
+    /**
+     * Check Service Readiness
+     * Answers 200 only when this PD is part of a raft quorum, that is, the 
raft node is active
+     * and knows the current leader. Otherwise answers 503 so that anything 
gating on PD
+     * (the compose healthcheck in front of Stores, a Kubernetes readiness 
probe)
+     * is held back until the PD can serve. Like /health this endpoint needs 
no authentication.
+     *
+     * @return JSON with the readiness flag, the local raft state and the raft 
address of the
+     * leader (null when there is none)
+     */
+    @GetMapping(value = "/ready", produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<Map<String, Object>> checkReady() {
+        RaftEngine raft = RaftEngine.getInstance();
+        boolean ready = raft.isReady();
+        State state = raft.getNodeState();
+        PeerId leader = raft.getLeader();

Review Comment:
   🧹 The body is built from four independent raft reads. `isReady()` reaches 
`getLeaderId()` through `hasLeader(node)`, then `getLeader()` calls it again, 
so a step-down between the two yields `"ready": true` with `"leader": null`, 
which `api-reference.md:802` promises only for the `503` case.
   
   Requested change: derive all four fields from a single `Node` reference, the 
same idiom as the `hasLeader(Node)` overload added in 5bd1b96. A small snapshot 
accessor on `RaftEngine` would do it without moving raft logic into the 
controller.



##########
docker/README.md:
##########
@@ -202,6 +202,15 @@ done
 curl -fsS http://localhost:8088/about
 ```
 
+PD answers two unauthenticated probe endpoints. `/v1/health` is liveness only:
+it returns `200` as soon as the REST listener is up, even when the PD has no
+raft leader. `/v1/ready` returns `200` only while the PD sees a leader and
+`503` otherwise, so the compose healthchecks gate Stores on `/v1/ready`. A
+single PD elects itself; three PDs become ready once two can talk to each
+other. `/v1/ready` first ships in 1.8.0: with an older `HUGEGRAPH_VERSION`
+the PD healthcheck never passes and the Stores never start, so pin 1.8.0
+or newer, or build the images from source with `docker-compose.dev.yml`.

Review Comment:
   ⚠️ This states the opposite of what happens, and my earlier comment on this 
paragraph was wrong to predict the hang.
   
   With an older `HUGEGRAPH_VERSION` the PD healthcheck passes on its first 
probe and the Stores start straight away, gated on nothing, for the reason in 
the comment on `docker-compose-hstore.yml:47`. That is the worse of the two 
failure modes: silent, and the same "healthy without a quorum" shape #3183 is 
about.
   
   Two smaller things in the same edit:
   
   - Lines 134 and 189 move the manual verification `curl -fsS` calls to 
`/v1/ready` as well, and those succeed the same way against an older image.
   - "1.8.0" is a guess. The tree builds as 1.7.0 and the next release number 
is not settled, so "the next release" is safer unless you have confirmation.
   
   Requested change: harden the healthchecks as described on the compose file, 
which makes this paragraph true as written. If they stay status-only, say 
instead that an older PD image answers `/v1/ready` with `200` and an auth error 
body, so the gate silently does nothing.



##########
hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/PDMetrics.java:
##########
@@ -76,7 +77,28 @@ private void registerMeters() {
         Gauge.builder(PREFIX + ".terms", () -> setTerms())
              .description("term of partitions in PD")
              .register(registry);
+        registerRaftMeters();
+    }
 
+    /**
+     * Raft membership gauges so operators can alert on quorum loss. They 
mirror what
+     * {@code GET /v1/ready} answers: a PD that sees no leader is outside a 
quorum.
+     */
+    private void registerRaftMeters() {
+        RaftEngine raft = RaftEngine.getInstance();
+        Gauge.builder(PREFIX + ".raft.leader", () -> raft.isLeader() ? 1 : 0)
+             .description("1 if this PD is the raft leader, 0 otherwise")
+             .register(registry);
+        Gauge.builder(PREFIX + ".raft.has_leader", () -> raft.hasLeader() ? 1 
: 0)
+             .description("1 if this PD sees a raft leader, i.e. is part of a 
quorum, 0 otherwise")
+             .register(registry);
+        Gauge.builder(PREFIX + ".raft.alive_peers", () -> {
+                 int alive = raft.getAlivePeerCount();
+                 return alive < 0 ? Double.NaN : alive;
+             })
+             .description("Number of raft peers, itself included, the leader 
has heard from " +
+                          "within the election timeout; NaN on non-leader 
nodes")

Review Comment:
   🧹 "within the election timeout" overstates the window. `listAlivePeers()` 
delegates to `NodeImpl.getAliveNodes`, which compares against 
`leaderLeaseTimeoutMs` (jraft-core 1.3.11, `NodeImpl.java:2247-2260`), and 
jraft derives that as `electionTimeoutMs * leaderLeaseTimeRatio / 100` with a 
default ratio of 90. `RaftEngine.java:110` does not override the ratio, so a 
peer drops out of the count at 90% of the election timeout, not at it.
   
   Small, but operators size alert windows off this string. Same wording in 
`RaftEngine.getAlivePeerCount()` (lines 257-258) and 
`hugegraph-pd/docs/api-reference.md:837`.
   
   ```suggestion
                .description("Number of raft peers, itself included, the leader 
has heard from " +
                             "within the leader lease timeout; NaN on 
non-leader nodes")
   ```



##########
hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/interceptor/AuthenticationConfigurer.java:
##########
@@ -32,6 +32,7 @@ public class AuthenticationConfigurer implements 
WebMvcConfigurer {
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(restAuthentication)
                 .addPathPatterns("/**")
-                .excludePathPatterns("/actuator/*", "/v1/health", 
"/v1/prom/targets/*");
+                .excludePathPatterns("/actuator/*", "/v1/health", "/v1/ready",
+                                     "/v1/prom/targets/*");

Review Comment:
   🧹 `/v1/ready` joins `/v1/health` as an anonymous path, but it answers with 
more than a status: the local `state` and the leader's raft address 
(`api-reference.md:795`). Everything else under `/v1` is auth-gated, and 
`/v1/health` returns an empty string.
   
   Minor rather than important, because `/actuator/*` is already anonymous and 
`Authentication`'s own class javadoc says PD must not be reachable outside a 
trusted network. Still, a probe only needs the status code, and this is the one 
new disclosure in the PR.
   
   Requested change: consider trimming the anonymous response to `ready`, and 
leaving `state`, `leader` and `isLeader` on an authenticated path for operators.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to