imbajin commented on code in PR #3130:
URL: https://github.com/apache/hugegraph/pull/3130#discussion_r3686348784


##########
hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java:
##########
@@ -169,6 +205,70 @@ protected AbstractStub setStubOption(AbstractStub value) {
                             config.getGrpcMaxOutboundMessageSize());
     }
 
+    private static boolean usesChannels(HgPair<ManagedChannel, ?>[] pairs,
+                                        ManagedChannel[] channels) {
+        if (pairs == null || pairs.length != channels.length) {
+            return false;
+        }
+        for (HgPair<ManagedChannel, ?> pair : pairs) {
+            if (pair == null || pair.getKey() == null ||
+                !containsChannel(channels, pair.getKey())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static boolean containsChannel(ManagedChannel[] channels,
+                                           ManagedChannel expected) {
+        return Arrays.stream(channels).anyMatch(channel -> channel == 
expected);
+    }
+
+    private void refreshChannelsIfAddressChanged(String target) {
+        long resolutionRequest = resolutionRequests.computeIfAbsent(target,
+                                                                    key -> new 
AtomicLong())
+                                                   .incrementAndGet();
+        String resolvedTarget = this.resolveTarget(target);
+        if (resolvedTarget.isEmpty()) {
+            return;
+        }
+        synchronized (channels) {
+            Long appliedResolution = appliedResolutions.get(target);
+            if (appliedResolution != null && appliedResolution >= 
resolutionRequest) {
+                return;
+            }
+            appliedResolutions.put(target, resolutionRequest);
+            String previousTarget = resolvedTargets.put(target, 
resolvedTarget);
+            if (previousTarget == null && !channels.containsKey(target)) {
+                return;
+            }
+            if (resolvedTarget.equals(previousTarget)) {
+                return;
+            }
+            ManagedChannel[] staleChannels = channels.remove(target);
+            if (staleChannels != null) {
+                Arrays.stream(staleChannels)
+                      .filter(channel -> channel != null && 
!channel.isShutdown())
+                      .forEach(ManagedChannel::shutdownNow);
+            }
+        }
+    }
+
+    protected String resolveTarget(String target) {
+        try {
+            String host = URI.create("dns://" + target).getHost();

Review Comment:
   ⚠️ 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.



##########
hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java:
##########
@@ -169,6 +205,70 @@ protected AbstractStub setStubOption(AbstractStub value) {
                             config.getGrpcMaxOutboundMessageSize());
     }
 
+    private static boolean usesChannels(HgPair<ManagedChannel, ?>[] pairs,
+                                        ManagedChannel[] channels) {
+        if (pairs == null || pairs.length != channels.length) {
+            return false;
+        }
+        for (HgPair<ManagedChannel, ?> pair : pairs) {
+            if (pair == null || pair.getKey() == null ||
+                !containsChannel(channels, pair.getKey())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static boolean containsChannel(ManagedChannel[] channels,
+                                           ManagedChannel expected) {
+        return Arrays.stream(channels).anyMatch(channel -> channel == 
expected);
+    }
+
+    private void refreshChannelsIfAddressChanged(String target) {
+        long resolutionRequest = resolutionRequests.computeIfAbsent(target,
+                                                                    key -> new 
AtomicLong())
+                                                   .incrementAndGet();
+        String resolvedTarget = this.resolveTarget(target);

Review Comment:
   ⚠️ 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.



##########
hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClientTest.java:
##########
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.store.client.grpc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Test;
+
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ManagedChannel;
+import io.grpc.MethodDescriptor;
+import io.grpc.stub.AbstractAsyncStub;
+import io.grpc.stub.AbstractBlockingStub;
+
+/**
+ * Verifies that Store address changes replace channels and their cached stubs 
safely.
+ */
+public class AbstractGrpcClientTest {
+
+    private static final AtomicInteger TARGET_SEQ = new AtomicInteger();
+
+    private static String uniqueTarget(String prefix) {
+        return prefix + "-" + TARGET_SEQ.incrementAndGet() + ":8500";
+    }
+
+    private static boolean belongsToPool(Channel channel,
+                                         ManagedChannel[] channels) {
+        return Arrays.stream(channels).anyMatch(current -> current == channel);
+    }
+
+    @Test
+    public void testAddressChangeReplacesChannelAndStubPools() {
+        String target = uniqueTarget("address-change");
+        RecordingGrpcClient client = new RecordingGrpcClient();
+        ManagedChannel[] oldChannels = client.getChannels(target);
+        assertNotNull(client.getBlockingStub(target));
+        assertNotNull(client.getAsyncStub(target));
+
+        client.resolvedTarget = "10.0.0.2";
+        ManagedChannel[] newChannels = client.getChannels(target);
+        assertNotSame("an address change must replace the channel pool",
+                      oldChannels, newChannels);
+        assertTrue("every stale channel must be shut down",
+                   
Arrays.stream(oldChannels).allMatch(ManagedChannel::isShutdown));
+
+        client.blockingStubChannels.clear();
+        client.asyncStubChannels.clear();
+        assertNotNull(client.getBlockingStub(target));
+        assertNotNull(client.getAsyncStub(target));
+        assertEquals("the blocking stub pool must be rebuilt",
+                     newChannels.length, client.blockingStubChannels.size());
+        assertTrue("replacement blocking stubs must use the new channel pool",
+                   client.blockingStubChannels.stream()
+                                              .allMatch(channel ->
+                                                        belongsToPool(channel, 
newChannels)));
+        assertEquals("the async stub pool must be rebuilt",
+                     newChannels.length, client.asyncStubChannels.size());
+        assertTrue("replacement async stubs must use the new channel pool",
+                   client.asyncStubChannels.stream()
+                                           .allMatch(channel ->
+                                                     belongsToPool(channel, 
newChannels)));
+    }
+
+    @Test
+    public void testFirstSuccessfulResolutionReplacesUnknownChannels() {
+        String target = uniqueTarget("first-successful-resolution");
+        RecordingGrpcClient client = new RecordingGrpcClient();
+        client.resolvedTarget = "";
+        ManagedChannel[] unknownChannels = client.getChannels(target);
+
+        client.resolvedTarget = "10.0.0.1";
+        ManagedChannel[] resolvedChannels = client.getChannels(target);
+        assertNotSame("a pool with unknown addresses must be replaced",
+                      unknownChannels, resolvedChannels);
+        assertTrue("every channel from the unknown pool must be shut down",
+                   
Arrays.stream(unknownChannels).allMatch(ManagedChannel::isShutdown));
+        assertTrue("the resolved channel pool must remain live",
+                   
Arrays.stream(resolvedChannels).noneMatch(ManagedChannel::isShutdown));
+    }
+
+    @Test
+    public void testOlderResolutionCannotReplaceNewerChannels() throws 
Exception {
+        String target = uniqueTarget("concurrent-address-change");
+        OutOfOrderResolverGrpcClient client = new 
OutOfOrderResolverGrpcClient();
+        ManagedChannel[] oldChannels = client.getChannels(target);
+        ExecutorService executor = Executors.newFixedThreadPool(2);
+
+        try {
+            Future<ManagedChannel[]> staleResolution =
+                    executor.submit(() -> client.getChannels(target));
+            assertTrue("the stale resolution must be in flight",
+                       client.staleResolutionStarted.await(5, 
TimeUnit.SECONDS));
+            Future<ManagedChannel[]> freshResolution =
+                    executor.submit(() -> client.getChannels(target));
+            ManagedChannel[] freshChannels = freshResolution.get(5, 
TimeUnit.SECONDS);
+
+            assertNotSame("the newer address must replace the old channel 
pool",
+                          oldChannels, freshChannels);
+            client.releaseStaleResolution.countDown();
+            assertSame("the late stale result must retain the newer channel 
pool",
+                       freshChannels, staleResolution.get(5, 
TimeUnit.SECONDS));
+            assertTrue("the replaced channel pool must be shut down",
+                       
Arrays.stream(oldChannels).allMatch(ManagedChannel::isShutdown));
+            assertTrue("the newer channel pool must remain live",
+                       
Arrays.stream(freshChannels).noneMatch(ManagedChannel::isShutdown));
+        } finally {
+            client.releaseStaleResolution.countDown();
+            executor.shutdownNow();
+        }
+    }
+
+    @Test
+    public void testStubBuildRetriesAfterChannelRefresh() throws Exception {

Review Comment:
   ⚠️ 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.



##########
hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java:
##########
@@ -169,6 +205,70 @@ protected AbstractStub setStubOption(AbstractStub value) {
                             config.getGrpcMaxOutboundMessageSize());
     }
 
+    private static boolean usesChannels(HgPair<ManagedChannel, ?>[] pairs,
+                                        ManagedChannel[] channels) {
+        if (pairs == null || pairs.length != channels.length) {
+            return false;
+        }
+        for (HgPair<ManagedChannel, ?> pair : pairs) {
+            if (pair == null || pair.getKey() == null ||
+                !containsChannel(channels, pair.getKey())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static boolean containsChannel(ManagedChannel[] channels,
+                                           ManagedChannel expected) {
+        return Arrays.stream(channels).anyMatch(channel -> channel == 
expected);
+    }
+
+    private void refreshChannelsIfAddressChanged(String target) {
+        long resolutionRequest = resolutionRequests.computeIfAbsent(target,
+                                                                    key -> new 
AtomicLong())
+                                                   .incrementAndGet();
+        String resolvedTarget = this.resolveTarget(target);
+        if (resolvedTarget.isEmpty()) {
+            return;
+        }
+        synchronized (channels) {
+            Long appliedResolution = appliedResolutions.get(target);
+            if (appliedResolution != null && appliedResolution >= 
resolutionRequest) {
+                return;
+            }
+            appliedResolutions.put(target, resolutionRequest);
+            String previousTarget = resolvedTargets.put(target, 
resolvedTarget);
+            if (previousTarget == null && !channels.containsKey(target)) {
+                return;
+            }
+            if (resolvedTarget.equals(previousTarget)) {
+                return;
+            }
+            ManagedChannel[] staleChannels = channels.remove(target);
+            if (staleChannels != null) {
+                Arrays.stream(staleChannels)
+                      .filter(channel -> channel != null && 
!channel.isShutdown())
+                      .forEach(ManagedChannel::shutdownNow);

Review Comment:
   ‼️ `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.



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