Skip to content

fix(store): refresh gRPC channels after address changes - #3130

Open
bitflicker64 wants to merge 3 commits into
apache:masterfrom
bitflicker64:fix/hstore-channel-refresh-3124
Open

fix(store): refresh gRPC channels after address changes#3130
bitflicker64 wants to merge 3 commits into
apache:masterfrom
bitflicker64:fix/hstore-channel-refresh-3124

Conversation

@bitflicker64

@bitflicker64 bitflicker64 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Purpose of the PR

HStore caches gRPC channels and blocking/asynchronous stubs by Store target.
When a stable target resolves to a different address, those pools can retain the
failed transport indefinitely even though the replacement Store is reachable.

This is the channel-lifecycle half of #3124. Complete recovery behind a stable
DNS name still requires the finite positive DNS TTL from #3126; this patch does
not bypass an indefinitely stale JVM DNS cache.

Main Changes

  • Fingerprint each target's sorted resolved address set before selecting a
    channel.
  • Throttle refresh checks with a per-target single-flight TTL so only one caller
    performs a slow DNS check while other callers keep using the cached healthy
    pool.
  • Publish replacement channel pools before retiring old pools; retire old
    channels with shutdown() first and use bounded delayed shutdownNow() only
    if they do not drain.
  • Rebuild blocking and asynchronous stub pools when their channels no longer
    belong to the current pool, and spread cached stubs across the channel pool.
  • Parse raw host:port, dns:///host:port, and bracketed IPv6 targets for
    fingerprinting; unsupported resolver schemes are left to gRPC and skipped by
    the DNS fingerprint refresh path.
  • Add nine self-contained refresh regression tests without a running PD or Store
    cluster.

Verifying these changes

  • Need tests and can be verified as follows:
    • JAVA_HOME=$(/usr/libexec/java_home -v 11) mvn test -pl hugegraph-store/hg-store-test -am -Dtest=AbstractGrpcClientTest -Dsurefire.failIfNoSpecifiedTests=false: AbstractGrpcClientTest ran 9 tests with 0 failures, errors, or skips on Java 11.
    • JAVA_HOME=$(/usr/libexec/java_home -v 11) mvn test -pl hugegraph-store/hg-store-test -am -P store-client-test -Dsurefire.failIfNoSpecifiedTests=false: ClientSuiteTest ran 9 tests with 0 failures, errors, or skips on Java 11.
    • JAVA_HOME=$(/usr/libexec/java_home -v 11) mvn editorconfig:format: passed with 0 files changed.
    • JAVA_HOME=$(/usr/libexec/java_home -v 11) mvn clean compile -Dmaven.javadoc.skip=true: passed across all 38 reactor modules on Java 11.

Does this PR potentially affect the following parts?

  • Dependencies
  • Modify configurations
  • The public API
  • Other affects
  • Nope

Documentation Status

  • Doc - TODO
  • Doc - Done
  • Doc - No Need

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. store Store module tests Add or improve test cases labels Jul 30, 2026
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
@bitflicker64
bitflicker64 force-pushed the fix/hstore-channel-refresh-3124 branch from 218b681 to 26218cb Compare July 30, 2026 13:21
@imbajin
imbajin requested a review from Copilot July 30, 2026 19:31

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.

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.

Comment on lines +117 to +122
IntStream.range(0, concurrency).forEach(i -> {
ManagedChannel channel = targetChannels[index];
AbstractBlockingStub stub = getBlockingStub(channel);
value[i] = new HgPair<>(channel, stub);
// log.info("create channel for {}",target);
});

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.

Validated as a real issue: the pool build loop used the selected request index, so every cached slot could bind to the same channel. Fixed in 95489135 by using the loop slot i for both blocking and async stub pools, and added spread assertions that the rebuilt pools cover every channel. Verified with the refreshed 9-test suite on Java 11.

@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: 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.

if (staleChannels != null) {
Arrays.stream(staleChannels)
.filter(channel -> channel != null && !channel.isShutdown())
.forEach(ManagedChannel::shutdownNow);

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.

‼️ shutdownNow() forcibly terminates existing calls on these channels, but callers may already hold blocking or streaming stubs after the cache lock is released. A normal DNS rotation can therefore cancel active Store operations and leave writes with ambiguous outcomes. Please publish the replacement pool first, gracefully retire old channels with shutdown(), and use bounded delayed forced termination only after draining; add an active-RPC/stream refresh test.

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.

Fixed in 95489135. The refresh path now creates and publishes the replacement pool before retiring the old pool. Retired channels receive graceful shutdown() first, and a bounded drain task only calls shutdownNow() if termination does not complete. Added testRefreshGracefullyRetiresActiveStreamChannels() to cover a held async/stream stub during refresh.

long resolutionRequest = resolutionRequests.computeIfAbsent(target,
key -> new AtomicLong())
.incrementAndGet();
String resolvedTarget = this.resolveTarget(target);

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.

⚠️ Every getChannels() call reaches this synchronous resolution path before consulting the cached pool, and Store session methods acquire a stub per request. When the JVM DNS cache expires or the resolver is slow, application request threads now block on InetAddress.getAllByName(). Please throttle/cache refreshes using a configurable interval or single-flight TTL, or refresh asynchronously while retaining the last healthy pool; cover repeated and concurrent stub acquisition with a delayed resolver.

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.

Fixed in 95489135. Refresh checks are now throttled by a per-target TTL and guarded with a try-lock, so only one caller performs DNS resolution while concurrent callers keep using the last cached healthy pool. Added repeated acquisition and delayed concurrent resolver coverage to verify this path.


protected String resolveTarget(String target) {
try {
String host = URI.create("dns://" + target).getHost();

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.

⚠️ This no longer matches the target contract used by ManagedChannelBuilder.forTarget(target). For example, a valid dns:///store:8500 target becomes dns://dns:///store:8500, so the fingerprint resolves the literal host dns; other gRPC resolver schemes can yield no host at all. The channel may still connect while refresh silently monitors the wrong endpoint. Please parse supported gRPC target schemes consistently (or explicitly validate and restrict the accepted format) and add URI-form and IPv6 target tests.

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.

Fixed in 95489135 for the target formats this client can safely fingerprint: raw host:port, dns:///host:port, and bracketed IPv6. Unsupported resolver schemes now return an empty fingerprint so the client does not silently monitor the wrong host while gRPC handles the original target. Broader non-DNS resolver fingerprinting is out of scope for this channel-lifecycle fix. Added URI-form, IPv6, and unsupported-scheme tests.

}

@Test
public void testStubBuildRetriesAfterChannelRefresh() throws Exception {

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.

⚠️ This interleaving test only exercises getBlockingStub(), while the PR independently rewrites the async refresh/retry/cache-publication path and builds async stubs in parallel. The sequential async assertion cannot catch a stale async pool published during refresh. Please add the equivalent latch-controlled async interleaving test and assert that both returned stubs and the final cache reference only current live channels.

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.

Fixed in 95489135. Added testAsyncStubBuildRetriesAfterChannelRefresh() with the same latch-controlled stale-build interleaving as the blocking path, and it asserts both returned async stubs plus the final async cache only reference current live channels.

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 1.54%. Comparing base (b9710a7) to head (9548913).

❗ There is a different number of reports uploaded between BASE (b9710a7) and HEAD (9548913). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (b9710a7) HEAD (9548913)
3 1
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #3130       +/-   ##
============================================
- Coverage     39.19%   1.54%   -37.65%     
+ Complexity      264      21      -243     
============================================
  Files           770     748       -22     
  Lines         65779   63264     -2515     
  Branches       8726    8278      -448     
============================================
- Hits          25779     975    -24804     
- Misses        37247   62205    +24958     
+ Partials       2753      84     -2669     

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

- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files. store Store module tests Add or improve test cases

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[Bug][HStore][Kubernetes] Data writes remain unavailable after Store pod replacement due to stale DNS resolution

3 participants