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


##########
hugegraph-pd/hg-pd-client/src/main/java/org/apache/hugegraph/pd/client/KvClient.java:
##########
@@ -127,7 +156,7 @@ public TTLResponse putTTL(String key, String value, long 
ttl) throws PDException
 
     private void onEvent(WatchResponse value, Consumer<T> consumer) {
         log.debug("receive message for {},event Count:{}", value, 
value.getEventsCount());
-        clientId.compareAndSet(0L, value.getClientId());
+        watchClientId.compareAndSet(0L, value.getClientId());

Review Comment:
   ‼️ Critical. The global watch ID can be restored by another live 
subscription while a failed subscription is waiting to reconnect. For example, 
A clears the ID after disconnecting, B receives an event and writes the old ID 
back here, then A reconnects with that old ID. The server registers observers 
with `putIfAbsent(key@clientId, observer)`, so A's stale server observer can 
win and the new stream appears started but never receives later events. Please 
use a per-subscription/generation ID or atomically replace the stale server 
observer, and test the one-failed/one-still-active sequence.



##########
hugegraph-pd/hg-pd-client/src/main/java/org/apache/hugegraph/pd/client/KvClient.java:
##########
@@ -56,13 +60,38 @@
 @Slf4j
 public class KvClient<T extends WatchResponse> extends AbstractClient 
implements Closeable {
 
-    private AtomicLong clientId = new AtomicLong(0);
-    private Semaphore semaphore = new Semaphore(1);
-    private AtomicBoolean closed = new AtomicBoolean(false);
-    private Set<StreamObserver> observers = ConcurrentHashMap.newKeySet();
+    private static final long RECONNECT_DELAY_MS = 1000L;
+    private static final Set<Status.Code> NON_RETRYABLE_WATCH_ERRORS =
+            Set.of(Status.Code.CANCELLED,

Review Comment:
   ⚠️ Important. `CANCELLED` is treated as permanently non-retryable, but 
`AbstractClient.resetStub()` closes the shared channel with `shutdownNow()`. 
That internal refresh can deliver `CANCELLED` to unrelated live watches, which 
then enter `stopWatch()` and are removed forever even though `KvClient` is 
still open. Please distinguish explicit client close from internal channel 
replacement, or otherwise make these cancellations recoverable, and add a 
multi-subscription channel-reset test.



##########
hugegraph-pd/hg-pd-client/src/main/java/org/apache/hugegraph/pd/client/KvClient.java:
##########
@@ -180,50 +200,170 @@ public void onNext(WatchResponse value) {
 
             @Override
             public void onError(Throwable t) {
-                release();
-                if (!closed.get()) {
-                    clientId.set(0);
-                    listenWrapper.accept(key, consumer);
+                if (isRetryableWatchError(t)) {
+                    requestReconnect(subscription, this);
+                } else {
+                    stopWatch(subscription, this, t);
                 }
             }
 
             @Override
             public void onCompleted() {
-
+                requestReconnect(subscription, this);
             }
         };
     }
 
     public void listen(String key, Consumer<T> consumer) throws PDException {
-        long value = clientId.get();
-        StreamObserver<WatchResponse> observer = getObserver(key, consumer, 
listenWrapper, value);
-        acquire();
+        listen(key, consumer, false);
+    }
+
+    public void listenPrefix(String prefix, Consumer<T> consumer) throws 
PDException {
+        listen(prefix, consumer, true);
+    }
+
+    private void listen(String key, Consumer<T> consumer, boolean prefix) 
throws PDException {
+        WatchSubscription subscription = new WatchSubscription(key, consumer, 
prefix);
+        subscriptions.add(subscription);
         try {
-            WatchRequest k =
-                    
WatchRequest.newBuilder().setClientId(clientId.get()).setKey(key).build();
-            streamingCall(KvServiceGrpc.getWatchMethod(), k, observer, 1);
-        } catch (Exception e) {
-            release();
-            throw new PDException(Pdpb.ErrorType.PD_UNREACHABLE_VALUE, e);
+            if (!startWatch(subscription)) {
+                throw new PDException(Pdpb.ErrorType.PD_UNREACHABLE_VALUE,
+                                      "KvClient is closed");
+            }
+        } catch (PDException e) {
+            subscription.observer.set(null);
+            subscriptions.remove(subscription);
+            throw e;
         }
     }
 
-    public void listenPrefix(String prefix, Consumer<T> consumer) throws 
PDException {
-        long value = clientId.get();
-        StreamObserver<WatchResponse> observer =
-                getObserver(prefix, consumer, prefixListenWrapper, value);
-        acquire();
+    private boolean startWatch(WatchSubscription subscription) throws 
PDException {
+        return startWatch(subscription, true);
+    }
+
+    private boolean startWatch(WatchSubscription subscription,
+                               boolean waitForPermit) throws PDException {
+        if (closed.get()) {
+            return false;
+        }
+
+        StreamObserver<WatchResponse> observer = getObserver(subscription);
+        subscription.observer.set(observer);
+        if (closed.get()) {
+            subscription.observer.compareAndSet(observer, null);
+            return false;
+        }
+
+        if (waitForPermit) {
+            acquire(watchClientId, watchSemaphore);
+        } else if (!tryAcquire(watchClientId, watchSemaphore)) {
+            subscription.observer.compareAndSet(observer, null);
+            return false;
+        }
+        if (closed.get()) {
+            subscription.observer.compareAndSet(observer, null);
+            release(watchSemaphore);
+            return false;
+        }
+
+        WatchRequest request = WatchRequest.newBuilder()
+                                           .setClientId(watchClientId.get())
+                                           .setKey(subscription.key)
+                                           .build();
         try {
-            WatchRequest k =
-                    
WatchRequest.newBuilder().setClientId(clientId.get()).setKey(prefix).build();
-            streamingCall(KvServiceGrpc.getWatchPrefixMethod(), k, observer, 
1);
+            if (subscription.prefix) {
+                streamingCall(KvServiceGrpc.getWatchPrefixMethod(), request, 
observer, 1);
+            } else {
+                streamingCall(KvServiceGrpc.getWatchMethod(), request, 
observer, 1);
+            }
+            return true;
         } catch (Exception e) {
-            release();
+            release(watchSemaphore);
+            if (e instanceof PDException) {
+                throw (PDException) e;
+            }
             throw new PDException(Pdpb.ErrorType.PD_UNREACHABLE_VALUE, e);
         }
     }
 
-    private void acquire() {
+    private void requestReconnect(WatchSubscription subscription,
+                                  StreamObserver<WatchResponse> 
sourceObserver) {
+        if (closed.get() ||
+            !subscription.observer.compareAndSet(sourceObserver, null)) {
+            return;
+        }
+        watchClientId.set(0L);

Review Comment:
   ‼️ Critical. `requestReconnect()` resets the observer and watch ID but 
leaves the cached async stub intact. After `Leader_Changed` or an asynchronous 
`UNAVAILABLE`, `reconnect()` calls `getStub()` and reuses the same channel; the 
server-side `onError` callback never enters `streamingCall()`'s synchronous 
peer retry. A leader move can therefore retry the old follower forever. Please 
invalidate/rotate the async stub before scheduling the reconnect and cover this 
with a real two-peer leader-failover test.



##########
hugegraph-pd/hg-pd-client/src/main/java/org/apache/hugegraph/pd/client/KvClient.java:
##########
@@ -237,7 +377,7 @@ private void acquire() {
         }
     }
 
-    private void release() {
+    private void release(Semaphore semaphore) {

Review Comment:
   ⚠️ Important. Permit ownership is still racy: this check-then-release is not 
atomic, and `tryAcquire()` can return true for a nonzero client ID without 
acquiring a permit while close/failure paths still call `release()`. Concurrent 
`Starting`, error, or close callbacks can therefore raise the permit count 
above one, allowing multiple reconnects to issue `clientId=0` streams and 
record inconsistent server IDs. Please model acquisition ownership explicitly 
(or use an atomic state machine) and assert that the effective permit count 
never exceeds one under concurrent callbacks.



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