bitflicker64 commented on PR #3132:
URL: https://github.com/apache/hugegraph/pull/3132#issuecomment-5611399757
## Campaign 2026-09-10: chart 0.1.6 defaults run against real images
The three image-side fixes this chart's defaults depend on are now on
`master`, so the
defaults committed in `2bff77a6` were run against real images rather than
reasoned about.
This is the runtime half of the checklist item whose docs half was done last
week. It also
turned up one defect in this PR's own `templates/`, at the bottom.
**What was under test.** Chart `0.1.6` from `hugegraph/hugegraph` tag
`helm-dev-20260910`
(`d8b6a0bf`). This PR's own `Chart.yaml` still declares `0.1.0`; what the
two share is the
template content and the three defaults, not the version string. Its
`templates/`, `values.schema.json` and `tests/` are byte-identical with
this PR at `2bff77a6` (the three tree hashes match), so every template-level
row applies
here too. `values-single.yaml` is byte-identical too, but `values.yaml` and
`values-cluster.yaml`
are **not**, and that matters for two rows below: the tested tree defaults
`hubble.enabled: true`, `pullPolicy: IfNotPresent` and the image tag
`helm-dev`, this PR
defaults them `false`, `Always` and `latest`. Anything that counts or scans
rendered
objects therefore differs between the two trees, and both numbers are given.
Two image sets were built from source with `docker buildx bake`, the
revision label stamped
in and read back from each running pod through its node, never from the
build host:
| Stage | Tag | Built from | Revision read back from the pods |
|---|---|---|---|
| **A** | `a0905` | tag `helm-dev-20260905` (`6ec19838`): master `36811483`
**plus #3185, #3187 and #3189 as pull-request branches** | `6ec1983889f58a...`
on all 9 pods |
| **B** | `b0910` | tag `helm-dev-20260910` (`d8b6a0bf`): **merged master
`60c8803d`** plus this chart | `d8b6a0bf256616...` on all 9 pods |
| both | `hubble:latest` | pulled, not built here; built by
hugegraph/actions from hugegraph-toolchain | `3b385c3d2f1c04...` |
Both sets therefore carry all three fixes, one as branches and one as merged
master, so
**this campaign is mostly a merge-regression check, not a
before-and-after**. There is one
exception, and it turned out to be the interesting row: #3185 gained a
further commit,
`a3b9395`, **after** the stage A tag was cut, so for the readiness handler's
own latency
stage A really is a pre-fix control. Everything else has no pre-fix control
in it. Every row
below says which stage it was measured on.
Host: a 4-node kind cluster (1 control-plane + 3 workers, Kubernetes
v1.37.0),
`values-cluster.yaml` plus a session file pinning the image tags and pull
policy, the
admin password, and the PD and Store PVC sizes at 2Gi and 5Gi rather than
the preset's 10Gi
and 50Gi, because the preset asks for 180Gi of PVCs across the six pods and
this host has
115 GB free. That last one is a deviation from the preset and the PVC
reattach row below
inherits it. **The 0.1.6 defaults were deliberately not
overridden**, which is the point: `pd.readinessPath`, `store.waitPath` and
`HG_SERVER_STARTUP_TIMEOUT_S` all came from the chart.
### The three new defaults
- **PD readiness on `/v1/ready`** (stage A install, both stages under
fault). All three PDs
were Ready when `helm install --wait` returned on stage A, with
`/v1/health` and
`/v1/ready` both answering 200 side by side. There was no stage B install:
stage B was a
`helm upgrade` of the same release, and its side-by-side 200s come from
the post-roll
check, not from an install. Under a 2-of-3 PD loss on **stage B** the
survivor answered
**503 from T+2 s to T+12 s** while `/v1/health` stayed 200 on **38 of 38**
samples, and
3/3 PD were Ready again about 5 s later.
- **The Store PD wait on `/v1/ready`** (stage A). The rendered Store
StatefulSet's init
container carries `WAIT_PATH="/v1/ready"`, and all three Stores left it,
so the majority
the gate counts is a quorum rather than a set of live listeners. (For the
record, the
Store's own three probes stay on its own `/v1/health`, and PD keeps
`/v1/health` on
startup and liveness so a PD that merely lost its leader is not restarted.
Only PD
readiness and the Store's PD wait moved.) The chart's own `NOTES.txt` still
tells the operator otherwise; see the last section.
- **A derived `HG_SERVER_STARTUP_TIMEOUT_S`** (stage B). The Pod spec
carries `"450"`,
exactly the startup probe budget (`failureThreshold: 90` x `periodSeconds:
5`). Caught
on a replacement Server three seconds after a delete, on the `b0910` image:
`/bin/bash ./bin/start-hugegraph.sh -j -Xms512m -Xmx1024m ...
-XX:+UseContainerSupport -t 450`.
A one-off Pod with an invalid value exits 1 with the image's range error
on **both**
image sets, so #3187's validation survived the merge.
### Results
| Row | Stage | Verdict | Evidence |
|---|---|---|---|
| static gates | chart only | PASS-static | lint on 3 presets; **63 unit
tests**; 15 of 15 fail-paths rejected, each with its recorded rejection text,
and 5 of 5 positive controls render (the exit code is the pass condition; their
stderr capture files are empty, so that row is thinner evidence than the
fail-paths); renders **19/17/20** on the tested tree and **16/14/16** on this
PR's own values, the difference being the Hubble objects the PR does not render
by default; kubeconform `-strict` clean on both trees at the 1.23.0 floor and
at 1.37.0 (0 invalid, 0 skipped); pluto clean on both; `ct lint` rc=0 on the
first attempt |
| policy scanners | chart only, tree-dependent | recorded, not clean | see
the section below: the headline numbers belong to the tested tree, not to this
PR, and this PR's own render was scanned separately |
| install + smoke | A | PASS-smoke | `helm install --wait` rc=0, 10/10
Ready, **0 restarts**, all ten pods at AGE 52s when it returned; `Could not
resolve allowlist entry` = 0 and `Blocked connection` = 0 on every PD; one
clean election; CRUD 202/201/201/200 |
| auth matrix | A and B | PASS | 401 / 401 / 200 on **every** Server Pod IP,
on both stages |
| `helm test` | B | PASS | `Phase: Succeeded` |
| Hubble | A and B | PASS | `operations/nodes` lists **3 PD, 3 STORE, 3
SERVER, all UP** with three distinct uptimes. Hubble itself was logged into
with the admin password the harness pinned, not with a PD credential; what the
row shows is that Hubble reached PD and enumerated the topology, and the only
PD credential in its rendered config is the one the chart wrote. That config
value was not captured to a file, so treat the chart-wrote-it half as derived |
| PD follower crash | A | PASS-hardening | 5 of 5 writes 202 during the
outage, rejoin 20 s, leadership unchanged |
| PD leader crash | A | PASS-hardening | new leader in **5 s**, pre-fault
write still readable |
| PD majority loss | A and B | PASS-hardening | see the `/v1/ready` numbers
above; stage A gave the same shape (503 at T+2, health 200 on 43 of 43) |
| Store crash + durability | A | PASS-hardening | mid-fault write 202, pre-
and mid-fault markers readable after rejoin |
| pod-IP churn | A | PASS | PD Pod IPs changed in two separate fault
scenarios and again at a full rolling replacement, with **0 blocked lines every
time** |
| PVC reattach | A | PASS-hardening | same `PersistentVolume` rebound, data
readable |
| rolling restart under load | A | PASS-hardening | 54 attempted, 52 acked,
**0 acked writes lost**, 3 of 3 uids per component |
| discovery lease, 4 claims | A | PASS-hardening | 3 rows = 3 Pod IPs at
`interval 15000`; new IP at T+10 s, old row gone by T+46 s, peak 4 rows;
`advertiseUrl` collapses to 1 row and the revert restores 3 |
| PD credential matrix | A and B | PASS | 401 **with** `WWW-Authenticate`
for a missing credential, an empty password, a wrong password, and for the two
unknown names tried (`admin`, `nobody`); 200 only for `hg`, `store`, `hubble`,
`vermeer` with the release secret; `/v1/health`, `/v1/ready` and the actuator
stay open. **Identical on both stages** |
| secret rotation | A | PASS-hardening | `checksum/auth` changed on both
upgrades and Server rolled both times; admin continuity unchanged by design |
| **image upgrade A to B** | A to B | PASS-hardening | PD 3/3, Store 3/3,
Server 3/3 rolled, **Hubble 0 of 1** because its image did not change;
`checksum/pd-auth` **unchanged**, so the roll is attributable to the image;
data written before the upgrade still readable (200 on three consecutive
reads); 12 of 12 writes through the Service afterwards |
| PD leader partition | A | **INCONCLUSIVE-confounded** | see below |
### The two policy scanners, and why their numbers are not this PR's
I ran both trees' cluster renders through the same two scanners, same
invocation, so the
comparison is like for like.
| Scanner | Tested tree (`helm-dev-20260910`) | This PR (`2bff77a6`) |
|---|---|---|
| kube-score, `--output-format ci` | **43 CRITICAL, 2 WARNING** | **36
CRITICAL, 0 WARNING** |
| polaris | **score 84**, 20 controllers | **score 83**, 16 controllers |
The gap is entirely the Hubble Deployment, which this PR's default values do
not render:
nine findings live on it, 7 CRITICAL and both WARNINGs, and 43 minus 7 is
exactly 36. The
three `ImagePullPolicy is not set to Always` findings on PD, Store and
Server disappear on
this PR, which defaults `pullPolicy: Always`, but three `Image with latest
tag` findings
appear in their place, because this PR leaves the image tag at `latest`
where the tested
tree pins it. Net zero on those, so **on the workloads this PR actually
ships, the two trees
score the same**. polaris's one point is worth a closer look, because it is
not simply the
smaller object count. The two failure profiles differ in severity, not only
in volume: the
tested tree has **33 warning-level and 6 danger-level** failures across its
20 controllers,
this PR **24 and 7** across 16. Fewer objects and *more* danger-level
findings. The cause is
that same swap: `pullPolicyNotAlways` is a warning and is satisfied on this
PR, while
`tagNotSpecified` is a danger and fires on PD, Store and Server here where
the tested tree
pins the tag. So the tag pin this PR already promises for 1.8.x removes
three danger-level
findings, and that is the more useful reading of the two scores. I have not
tried to
reproduce polaris's exact weighting, so take the two published scores as
given and the
severity counts, which come straight out of the two JSON reports, as the
explanation.
One bookkeeping note, since I quoted a different figure in an earlier draft
of this comment:
kube-score's default output groups findings by check per object and gives 27
CRITICAL on the
tested tree, where `ci` lists each finding and gives 43. Same run of the
same tool on the
same file, different granularity. Every number in this section is `ci`.
The classes themselves are the familiar ones: NetworkPolicy, root user,
ephemeral storage,
`readOnlyRootFilesystem`, pull policy and image tag, all of which map to an
existing
documented decision. Two do not fit that list and should be said out loud:
- **`Container has the same readiness and liveness probe`, 2 findings, on
this PR's render
as well as the tested tree.** This one **shrank** because of this change:
it fires on
Server and Store only, not on PD, because PD readiness left `/v1/health`
and its two
probes are no longer the same signal. That is a scanner independently
confirming the
second half of the open review-thread objection.
- **`No matching PodDisruptionBudget was found`, 1 finding, on the Server
Deployment, on
this PR's render as well.** The chart ships PDBs for PD and Store but not
for Server. I
have no documented decision to point at for that, so I am flagging it
rather than
explaining it away.
### The partition scenario, reported as inconclusive
Two attempts, neither of which supports a verdict, both recorded rather than
dropped:
- A node-level `iptables FORWARD` cut of the PD leader landed (735 packets
in, 132 out) and
raft did the right thing, moving leadership to a survivor. But **all ten
write samples
timed out at 15 s for the duration**. That is not a chart result: kubelet
probes originate
in the node netns and bypass FORWARD, so the partitioned PD stayed Ready,
stayed a
`pd-client` endpoint and stayed in every Server's static peer list. The
ambiguity was
declared before the fault.
- A cut inside the Pod's own network namespace removed that ambiguity, and
the PD did go
`podReady=false`, which is the readiness signal doing its job and is not
what a
`/v1/health` readiness would have done. Everything else about that run is
confounded: the
teardown read the container PID rather than the Pod **sandbox** PID, which
survives
container restarts, so the fault stayed in place far longer than intended,
and the host
was measurably short of memory at the time. The cluster recovered on its
own the moment
the harness rules were removed. Whether the EndpointSlice actually dropped
the address is
**not** claimed: the column meant to show it listed addresses regardless
of their ready
condition, so it measured nothing.
A clean single-node leader partition with a write oracle is therefore
**NOT-RUN**.
### Comparison with 2026-09-05
**No regressions.** The merged forms of #3185, #3187 and #3189 behave the
same as the
pull-request forms measured on 2026-09-05, with one exception in the merge's
favour:
#3185's merged form answers `/v1/ready` during an election three orders of
magnitude
faster, for the reason set out in the next section. The one-off Server
first-boot exit 1 seen then
did not reproduce: 0 restarts across all ten pods. Unit tests went 58 to 63
with the 0.1.6
guards. `ct lint` now passes on the first attempt, because the missing
`yamale` binary that
failed it on 2026-09-01 is installed on this host.
Install looks faster, and I want to be careful about that one, because the
two figures are
not the same measurement on the same machine. 2026-09-05 recorded an install
**duration** of
2 min 47 s on soul, a 24-core, 59 GB host (that campaign's `state.md`
records the specs).
Today's number is a pod **AGE** of 52 s on library, 12 cores and 15 GB by
`nproc` and
`free`, with the closest comparable figure being the 53 s span of the
install script
itself. Faster, yes, but do not read a 3x from it.
`hugegraph/hubble:latest` **now carries an
`org.opencontainers.image.revision` label**
(`3b385c3d`). Earlier campaigns recorded it as unlabelled and therefore not
attributable to
a source revision, so the whole topology is attributable at last.
### The readiness handler latency, and a correction to how I first read it
On 2026-09-05 I reported on apache/hugegraph#3185 that the `/v1/ready`
handler took 9.8 s to
answer its 503 during an election. It reproduced at **9.451460 s on the
stage A images** and
**did not reproduce on merged master**: across **eleven consecutive samples
inside the
leaderless window**, T+2 to T+12, the slowest answer was **3.055
milliseconds**.
I first wrote that up as the stall being intermittent. It is not. It is
fixed, and the fix
is in the tree under test. `a3b9395`, "serve readiness from raft callbacks,
not the node
lock", was pushed to #3185 on 2026-09-05 in response to that report, and it
reached master
in the squash `e85b36a4` on 2026-09-08. The stage A tag `helm-dev-20260905`
was cut before
it: `ProbeView` is absent from `6ec19838` and present in `60c8803d` and
`d8b6a0bf`. So the
two numbers are a genuine before and after of one commit, not two samples of
one flaky
path, and the comment that commit carries states the mechanism outright,
that jraft holds
the node lock while it reconnects to peers during an election and a reader
stalls for the
connect timeout.
That makes stage A a pre-fix control for this one row and nothing else, and
it is the only
before-and-after in the campaign.
A second correction, to my own method rather than to the code: the first
attempt at that
scenario sampled the prometheus gauge and both probes in one loop iteration.
The gauge call
stalled during the election, the loop jumped from T+5 to T+28, and the whole
leaderless
window fell into the gap, so the run would have been written up as "the
endpoint never
refused". Sampling `/v1/ready` alone at 1 s exposed it immediately. As a
consequence,
`hg_raft_has_leader` reaching 0 is **carried from 2026-09-05, not measured
today**; today's
gauge sampler was too coarse to catch a 12-second window and that cell is
inconclusive.
### Not run, stated as gaps
- **Upgrade from a released chart version: NOT-RUN.** There is still no
1.8.x release or
image tag to pin to or upgrade from, which remains the external gate on
this PR.
- **kubescape: NOT-RUN**, the binary is not on the test host.
- **A clean single-node PD leader partition with a write oracle: NOT-RUN**,
per the section
above.
- **EndpointSlice ready-condition membership under a partition: NOT-RUN.**
- Store majority loss, and the cross-namespace and node-freeze scenarios,
were not
attempted.
Two limits on what the S11 chain proves: no Server start slower than 120 s
occurred, so the
last link, the start script honouring `-t`, is still carried from #3187's
own tests rather
than observed here; and the claim that a pre-#3185 image would never turn
Ready on
`/v1/ready` is derived, not shown, because no such image exists in this
campaign.
One operational note for the docs rather than the code: an image-tag upgrade
replaces all
nine PD, Store and Server pods, and for a few minutes afterwards writes
through the Server
Service can stall even though every pod reports Ready. It cleared with no
intervention here,
and writes through both the Service and direct to pods succeeded afterwards,
but the exact
mechanism was not instrumented, only excluded: host load and memory were
fine, every PD and
Store was reachable and Up, and direct-to-pod writes were already succeeding
while
Service-routed ones were still timing out.
### Three lines to fix in this PR
Chart 0.1.6 moved the two defaults and updated most of the prose around
them, but three
places still describe the old default, and all three ship here exactly as
written.
`templates/NOTES.txt` is byte-identical between the tested tree and
`2bff77a6`. The two
READMEs are not identical overall, but the two parameter-table rows below
are word for word
the same on both.
**1. `templates/NOTES.txt`, lines 31 and 32.** Printed to the operator on
every install and
every upgrade. It appears verbatim in this campaign's install log, upgrade
log and
`helm test` output:
```
Store Pods stay in Init until a majority of PD peers answer store.waitPath
(/v1/health by default, which proves the listener is up, not a raft quorum).
```
**2 and 3. The two `README.md` parameter-table rows**, where the Default
column still reads
`/v1/health` and the description still tells the reader to set `/v1/ready`
themselves:
| Parameter | Description as shipped | Default column |
|---|---|---|
| `pd.readinessPath` | "`/v1/health` is liveness only; set `/v1/ready` on PD
images from 1.8.0 that carry it, never on older images" | `/v1/health` |
| `store.waitPath` | "`/v1/health` counts listeners; `/v1/ready` counts
quorum members but exists only on PD images from 1.8.0" | `/v1/health` |
Both now say the opposite of `values.yaml`, of this README's own bullet at
line 48 ("The
default `/v1/ready` stays 503 until a raft leader exists"), of the
Limitations section at
line 935 ("The chart defaults `pd.readinessPath` and `store.waitPath` to
`/v1/ready`"), and
of `values.schema.json`, which already describes `/v1/ready` as the default
in both places.
The rows themselves are at lines 306 and 336.
The parameter table is the reference an operator actually consults before
overriding
anything, and `NOTES.txt` is the only thing they are shown without asking.
Between them
they tell a reader that the quorum-aware gate is opt-in, which is the exact
point the open
review thread on `store-statefulset.yaml` is about, and it is no longer
true. It is three
lines, and I would rather flag them than push a fix into a PR under review
without asking.
--
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]