fix(store): recover retries after Store replacement - #3130
Conversation
218b681 to
26218cb
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the HStore gRPC client channel/stub lifecycle so that when a stable Store target (e.g., DNS name) resolves to a different address set, the client discards the prior channel pool and rebuilds related stub pools to avoid getting stuck on failed transports (issue #3124).
Changes:
- Add per-target “resolved address set” fingerprinting and retire/replace cached channel pools when the fingerprint changes (or when a previously-unresolved target first resolves).
- Rebuild blocking and async stub pools when they no longer correspond to the current channel pool, with concurrency ordering to prevent stale work from reintroducing retired channels.
- Add regression tests covering address changes and concurrent refresh/stub-build interleavings.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java | Adds resolved-address fingerprinting, refresh/retire logic for channel pools, and stub-pool rebuild safeguards under concurrent refresh. |
| hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClientTest.java | Adds refresh-focused tests validating channel replacement, stub-pool rebuild, and concurrency ordering behavior. |
| hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/ClientSuiteTest.java | Introduces a small suite to run the refresh regression tests together. |
Comments suppressed due to low confidence (1)
hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java:172
- When (re)building the async stub pool, the loop always selects
targetChannels[index]for every slot, so all cached async stubs share a single channel. This defeats the channel pool and undermines concurrency/failover across channels. Bind each stub to its corresponding channel (or at least distribute across the pool) by using the loop index.
IntStream.range(0, concurrency).parallel().forEach(i -> {
ManagedChannel channel = targetChannels[index];
AbstractAsyncStub stub = getAsyncStub(channel);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: Address refresh can abort active RPCs, adds synchronous DNS resolution to every request path, and does not preserve the existing gRPC target contract; the async refresh race also lacks equivalent coverage. Evidence: static review across six independent lanes; mvn test -pl hugegraph-store/hg-store-test -am -Dtest=AbstractGrpcClientTest -DfailIfNoTests=false passed 4 tests.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3130 +/- ##
============================================
- Coverage 39.23% 0.34% -38.90%
+ Complexity 264 74 -190
============================================
Files 771 749 -22
Lines 65938 63447 -2491
Branches 8759 8318 -441
============================================
- Hits 25872 220 -25652
- Misses 37310 63204 +25894
+ Partials 2756 23 -2733 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Refresh cached channel pools when a Store target resolves to a new address. Rebuild stale blocking and async stub pools, and guard concurrent resolution and publication races. Fixes apache#3124
- move graceful retirement to a cleanup scheduler - force-close partial pools after creation failures - validate cached stubs against channels by index - cover saturation, interruption, and drain deadlines
a08ac4b to
ddeef7a
Compare
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: DNS refresh can fail under the default security policy, and QueryV2 can race channel retirement. Evidence: six independent exact-head lanes; 14/14 focused Java tests and git diff --check passed; all visible exact-head checks passed.
Channel refresh resolved DNS on whichever thread asked for a stub. Under the default launcher that thread can be a Gremlin worker, and HugeSecurityManager denies it socket access, so an HStore-backed request could fail with a SecurityException instead of using the healthy pool. - run resolution, replacement creation and retirement on a channel maintenance executor, keeping the last healthy pool when resolution fails or times out - build the first pool for a target once its address is known, so a cold start no longer creates and immediately retires a pool - replace the per-target refresh lock with a single-flight task map that the cold path can also wait on, and throttle from both submission and completion - route QueryV2Client through the guarded async stub path instead of taking a channel straight from the pool, and restrict getChannels to subclasses - drop the channel monitor from stub acquisition: publishing a pool before retiring the previous one already orders the check, and the monitor is static - log refresh failures and pool replacements, which the executor otherwise discards, and never let a denied thread creation wedge refresh for a target - parse targets with URI, rejecting resolver schemes such as unix:/path that were resolved as a host named after the scheme - fold the blocking and async stub acquisition loops into one implementation
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. The initial refresh deadline can suppress DNS monitoring for the lifetime of a target when the JVM's nanoTime origin is negative; three independent current-head review lanes converged on this issue.
…tore disabled The Server wrapper now writes auth.admin_pa from the auth Secret alongside usePD and pd.peers, so an auth-enabled release keeps its configured admin password with init_store.enabled=false instead of silently falling back to the public default. The Secret value is rejected when it contains properties-parser metacharacters that would inject config lines or store a different password than the Secret holds. The new hubble component deploys the Hubble UI as a single-replica Deployment with pd and direct wiring modes, optional Ingress and H2 persistence, schema validation, render-time guards, docs, and CI coverage. PD-meta installs (auth enabled, or Hubble in pd mode) also announce the Server client Service URL to PD via server.urls_to_pd and server.deploy_in_k8s so discovery clients receive a resolvable address instead of the 0.0.0.0 default, and the Hubble wrapper writes server.host so current images bind all interfaces. Because current Hubble images authenticate their login against the cluster, rendering Hubble without server.auth fails unless explicitly overridden. The CI invalid-value step now fails on every case rather than only its last line, and positive renders cover both Hubble modes. Validated against a composition of master 1716c77 plus the current heads of apache#3119 (edf07d0), apache#3126 (b40c42f), and apache#3130 (198de19): fresh auth-enabled installs reach Ready with zero restarts, the admin credential comes from the Secret while unauthenticated and default-password requests get 401, and Hubble logs in with the Secret credential and reads cluster metadata through PD discovery, with its H2 metadata persisted on the PVC.
|
Codecov follow-up for the earlier 0% patch report at |
|
Final-head CI note for 6beea6f: all 17 GitHub checks passed, and the Store job generated and discovered the expected JaCoCo reports. Its legacy codecov-action v3.0.0 upload was rejected by Codecov with HTTP 429 and an expected retry window of 1,703 seconds, after which the action intentionally exited 0; consequently Codecov never registered the final commit. The cooldown has elapsed, but GitHub does not allow the fork contributor to rerun the completed Apache job. I am briefly closing and reopening this PR to retrigger trusted CI on the unchanged SHA, without modifying history or the four-file diff. The regenerated Java 11 Store profile still passes 27/27 and records QueryV2 line 47 as mi=0, ci=5 and line 58 as mb=0, cb=2. |
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: The refresh implementation still has a non-atomic stub-to-channel handoff and a shared resolver pool that can pause refreshes for every target when DNS blocks. Evidence: exact-head static review across six independent lanes; current-head checks are green, while these races remain distinct from the previously resolved SecurityManager, target parsing, stub distribution, and nanoTime findings.
…tore disabled The Server wrapper now writes auth.admin_pa from the auth Secret alongside usePD and pd.peers, so an auth-enabled release keeps its configured admin password with init_store.enabled=false instead of silently falling back to the public default. The Secret value is rejected when it contains properties-parser metacharacters that would inject config lines or store a different password than the Secret holds. The new hubble component deploys the Hubble UI as a single-replica Deployment with pd and direct wiring modes, optional Ingress and H2 persistence, schema validation, render-time guards, docs, and CI coverage. PD-meta installs (auth enabled, or Hubble in pd mode) also announce the Server client Service URL to PD via server.urls_to_pd and server.deploy_in_k8s so discovery clients receive a resolvable address instead of the 0.0.0.0 default, and the Hubble wrapper writes server.host so current images bind all interfaces. Because current Hubble images authenticate their login against the cluster, rendering Hubble without server.auth fails unless explicitly overridden. The CI invalid-value step now fails on every case rather than only its last line, and positive renders cover both Hubble modes. Validated against a composition of master 1716c77 plus the current heads of apache#3119 (edf07d0), apache#3126 (b40c42f), and apache#3130 (198de19): fresh auth-enabled installs reach Ready with zero restarts, the admin credential comes from the Secret while unauthenticated and default-password requests get 401, and Hubble logs in with the Secret credential and reads cluster metadata through PD discovery, with its H2 metadata persisted on the PVC.
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: Cold-target initialization can still run channel creation from a restricted Gremlin request, and the fingerprint parser accepts a DNS target form that gRPC 1.39 cannot build. Evidence: AbstractGrpcClient.java lines 131, 490, and 546; ExecutorPool.createExecutor() uses a lazy SynchronousQueue executor with CallerRunsPolicy; gRPC 1.39 DnsNameResolverProvider requires a slash-prefixed URI path.
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: The change eagerly creates 129 executor threads, retains target-keyed state indefinitely, and can publish a channel pool containing null entries after an Error. Focused validation passed 29/29 tests, while the latest dependency-check and Codecov checks are failing. Evidence: static review of AbstractGrpcClient.java:54-93 and 444-485; mvn -q test -pl hugegraph-store/hg-store-test -am -Dtest=AbstractGrpcClientTest -Dsurefire.failIfNoSpecifiedTests=false -Djacoco.skip=true; gh pr checks 3130 -R apache/hugegraph.
|
Current-head CI triage for fad20a3: the dependency-check job did not execute dependency analysis; actions/setup-java failed while downloading JDK 11 after repeated socket hang up errors, so that failure is external setup/network noise. The Codecov failures are current and substantive as reported: patch coverage is 0% for two changed QueryV2Client lines and project coverage is 34.73%. The Store job itself passed, but its uploaded aggregate coverage did not demonstrate those changed lines to Codecov. The three new exact-head review findings were validated and answered inline; all three require code/lifecycle follow-up before the review can be considered clear. |
Design direction to validate: keep Store identity at the DNS target layerThe current head ( In Kubernetes, HugeGraph should use a stable logical target such as flowchart LR
HG["HugeGraph Store client<br/>stable DNS target"]
CH["gRPC ManagedChannel"]
NR["gRPC NameResolver<br/>resolve + reconnect"]
DNS["Kubernetes DNS"]
OLD["old Store Pod<br/>10.0.0.8"]
NEW["replacement Store Pod<br/>10.0.0.19"]
HG --> CH --> NR --> DNS
DNS -. "before replacement" .-> OLD
DNS -- "after replacement" --> NEW
Under this model, HugeGraph does not need IP fingerprints, refresh deadlines, DNS worker pools, or a second connection-replacement state machine. It keeps the stable target and invalidates only the failed logical or transport state required for retry. The validation plan is:
Three independent tracks will validate this direction: gRPC and JDK source behavior, a minimal Java 11 runtime experiment, and a blind root-cause analysis. |
Validation update: native gRPC recovery works; refocus this PR on the HugeGraph lifecycleThree independent tracks reached the same conclusion: gRPC and JDK source analysis, a Java 11 runtime experiment, and a blind root-cause analysis. Native gRPC recoveryThe final Server distribution uses gRPC 1.47.0 for the behavior-driving api/core/netty/stub artifacts. Protobuf-related artifacts remain at 1.39.0, so the runtime is a mixed classpath. The runtime experiment used Java 11.0.18,
The relevant gRPC path is: This shows that a Store IP change does not require HugeGraph to destroy and rebuild its channel pool. HugeGraph lifecycle gapThe PR evicts the failed The retry must replace a cached session when its node is no longer current. The regression needs the same Store ID and stable DNS target and must prove that the same transaction retry opens a current session. Refactor boundaryflowchart LR
K8S["Kubernetes<br/>Pod IP + DNS"]
JDK["Java DNS policy<br/>finite TTL from #3126"]
GRPC["gRPC ManagedChannel<br/>resolve + reconnect"]
HG["HugeGraph<br/>node + session + retry"]
K8S --> JDK --> GRPC --> HG
The refactor should remove the external IP fingerprint, five-second polling, refresh executors, and whole-pool replacement. HugeGraph should keep the stable DNS target and fix only its node, session, retry, and explicit shutdown lifecycle. The remaining integration test is a resource-bounded Kubernetes run on the exact |
K8s A/B result: the current PR cannot recover the in-flight retryI ran a
The exact merge includes #3126 and uses This reproduces the source-level finding: the current PR invalidates the failed node and target, but the same transaction cannot replace its stale session. Before merge, the PR needs to:
Short client-side timeouts can leave Server-side requests running, so the final acceptance test must use a unique operation ID and distinguish an observed HTTP response from a later commit. |
imbajin
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: No new actionable findings were identified on the exact head after local review and focused Java 11 validation. Evidence: mvn -q test -pl hugegraph-store/hg-store-test -am -Dtest=AbstractGrpcClientTest -Dsurefire.failIfNoSpecifiedTests=false -Djacoco.skip=true (37/37 passed); gh -R apache/hugegraph pr checks 3130 (17/17 passed).
- remove periodic DNS polling and maintenance executors - invalidate only failed Store target channels on UNAVAILABLE - replace evicted node sessions atomically during retry - add focused lifecycle and concurrency regressions
Final result: what changed and what the Kubernetes test provesThe final refactor is on head Before and afterThe previous version assumed that HugeGraph had to detect every Store IP change itself. It added five-second DNS polling, address fingerprints, several maintenance executors, and full channel-pool replacement. Runtime testing showed that this duplicated work already owned by Kubernetes DNS, the JVM DNS policy from #3126, and gRPC name resolution. The actual failure was in HugeGraph's retry state. After The final implementation keeps the stable Store FQDN and makes three focused changes:
This reduced the PR from 2,295 to 486 added lines. Production additions fell from 732 to 140, and Exact Helm/Kubernetes acceptanceThe exact
These results cover both leader movement and follower replacement while Kubernetes changes Pod IPs behind stable StatefulSet DNS names. They support the PR's intended Store reconnect and session-rebind path; they do not claim cross-process exactly-once commit behavior. Other verification
Future TODOsThese items are not blockers for this PR:
|
Purpose
Closes #3124.
In Kubernetes, a Store keeps the same DNS name and Store ID when its Pod is replaced, but its IP can change. HugeGraph already connects through that stable DNS name. The missing piece was the retry path: after a transport failure, the same transaction could keep using the session bound to the old Store process.
flowchart LR subgraph Before["Before"] direction TB B1["Store Pod gets a new IP"] --> B2["transport fails"] B2 --> B3["retry reuses old Node + Session"] B3 --> B4["request fails"] end subgraph After["After"] direction TB A1["Store Pod gets a new IP"] --> A2["transport fails with UNAVAILABLE"] A2 --> A3["discard exact failed Node + target"] A3 --> A4["retry opens current Node + Session"] A4 --> A5["gRPC reconnects to new IP"] A5 --> A6["same request succeeds"] endWhat changed
UNAVAILABLE, discard only the exact failed Store node, close that target, and replace its cached session during the same transaction retry.CANCELLEDand unrelated failures do not evict the node.This relies on the finite JVM DNS TTL from #3126 and follows the ownership boundaries documented by Kubernetes StatefulSets, Java 11 DNS cache properties, and gRPC's NameResolver.
Verification
store-client-test: 9 tests, 0 failures, 0 errors, 0 skipped.git diff --check: passed.master + PRDocker build: 38/38 modules passed.201after about 31 secondsUNAVAILABLE → NOT_WORK → retry, and the 120-vertex request returned201after about 35 seconds201immediatelygRPC client is closedor OOM event was observedThe runtime uses gRPC api/core/netty/stub 1.47.0 for this behavior. Protobuf-related artifacts remain at 1.39.0.
Scope
This PR fixes Store replacement recovery. It does not add cross-process exactly-once commit or deduplication semantics, and it does not add a second DNS or reconnect state machine.
Documentation
No user-facing configuration or dependency is added.