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


##########
hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java:
##########
@@ -34,10 +44,53 @@
 import io.grpc.stub.AbstractAsyncStub;
 import io.grpc.stub.AbstractBlockingStub;
 import io.grpc.stub.AbstractStub;
+import lombok.extern.slf4j.Slf4j;
 
+@Slf4j
 public abstract class AbstractGrpcClient {
 
     protected static Map<String, ManagedChannel[]> channels = new 
ConcurrentHashMap<>();
+    private static final Map<String, String> resolvedTargets = new 
ConcurrentHashMap<>();
+    // A null deadline is the explicit "never scheduled" state; every long is 
a valid clock value.
+    private static final Map<String, AtomicReference<Long>> nextResolutions =
+            new ConcurrentHashMap<>();
+    private static final Map<String, CompletableFuture<Void>> refreshTasks =
+            new ConcurrentHashMap<>();
+    /*
+     * Refresh runs here rather than on a request thread: a caller of 
getChannels() may hold a
+     * Gremlin worker stack, which HugeSecurityManager denies socket access 
to. Creating the very
+     * first pool for a target is still done by the caller, so that path stays 
exposed.
+     */
+    private static final ScheduledThreadPoolExecutor 
CHANNEL_MAINTENANCE_EXECUTOR =

Review Comment:
   ⚠️ Two blocked DNS lookups can stop address refresh for every target. 
Evidence: CHANNEL_MAINTENANCE_EXECUTOR has only two threads, and each refresh 
calls resolveTarget at :307, which reaches InetAddress.getAllByName without a 
client deadline; refreshTasks is cleared only after that call returns. If two 
targets hang, later targets queue behind them and keep stale channel pools. 
Isolate resolution per target or use a resolver with a real 
timeout/cancellation, and add a regression where two blocked lookups do not 
prevent a third target from refreshing.



##########
hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java:
##########
@@ -91,31 +134,60 @@ public ManagedChannel[] getChannels(String target) {
     public abstract AbstractBlockingStub getBlockingStub(ManagedChannel 
channel);
 
     public AbstractBlockingStub getBlockingStub(String target) {
-        ManagedChannel[] channels = getChannels(target);
-        HgPair<ManagedChannel, AbstractBlockingStub>[] pairs = 
blockingStubs.get(target);
+        return this.acquireStub(target, this.blockingStubs, 
this::getBlockingStub,
+                                stub -> (AbstractBlockingStub) 
this.setBlockingStubOption(stub));
+    }
+
+    /**
+     * Returns a cached stub bound to a channel of the target's current pool, 
rebuilding the
+     * cache when the pool has been replaced. The stub comes from the pool 
that was published at
+     * the last check; a refresh landing immediately afterwards can still 
retire that pool, so
+     * callers are not shielded from an in-flight replacement.
+     *
+     * <p>The pool check needs no lock: the pool is published before the 
previous one is retired,
+     * so reading the current pool from the map is enough to know retirement 
has not started.
+     */
+    @SuppressWarnings("unchecked")
+    private <S> S acquireStub(String target,
+                              Map<String, HgPair<ManagedChannel, S>[]> 
stubCache,
+                              Function<ManagedChannel, S> stubFactory,
+                              Function<S, S> stubOption) {
+        while (true) {
+            ManagedChannel[] targetChannels = this.getChannels(target);
+            HgPair<ManagedChannel, S>[] pairs = stubCache.get(target);
+            int index = nextStubIndex();
+            if (!usesChannels(pairs, targetChannels)) {
+                synchronized (stubCache) {
+                    pairs = stubCache.get(target);
+                    if (!usesChannels(pairs, targetChannels)) {
+                        HgPair<ManagedChannel, S>[] value = new 
HgPair[concurrency];
+                        IntStream.range(0, concurrency).forEach(i -> {
+                            ManagedChannel channel = targetChannels[i];
+                            value[i] = new HgPair<>(channel, 
stubFactory.apply(channel));
+                        });
+                        S configuredStub = 
stubOption.apply(value[index].getValue());
+                        if (channels.get(target) != targetChannels) {
+                            continue;
+                        }
+                        stubCache.put(target, value);
+                        return configuredStub;
+                    }
+                }
+            }
+            S configuredStub = stubOption.apply(pairs[index].getValue());
+            if (channels.get(target) != targetChannels) {

Review Comment:
   ‼️ The identity check does not protect the handoff from returning a stub 
bound to a channel that is being retired. Evidence: acquireStub checks 
channels.get(target) at :178 and then returns; a concurrent refresh publishes 
the replacement at :339 and calls retireChannels at :349, whose shutdown 
immediately rejects new RPCs on the old pool. A caller can pass the check, be 
preempted, and receive the stub after retirement begins. Protect stub 
acquisition through first-call creation with a pool lease or read/write lock, 
or retry through a wrapper that binds to the current pool.



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