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


##########
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);
+        release(watchSemaphore);
+        scheduleReconnect(subscription);
+    }
+
+    private static boolean isRetryableWatchError(Throwable throwable) {
+        Status.Code code = Status.fromThrowable(throwable).getCode();
+        return !NON_RETRYABLE_WATCH_ERRORS.contains(code);
+    }
+
+    private void stopWatch(WatchSubscription subscription,
+                           StreamObserver<WatchResponse> sourceObserver,
+                           Throwable throwable) {
+        if (!subscription.observer.compareAndSet(sourceObserver, null)) {
+            return;
+        }
+        release(watchSemaphore);
+        subscriptions.remove(subscription);
+        log.error("Watch for key {} stopped after a non-retryable error: {}",
+                  subscription.key, Status.fromThrowable(throwable), 
throwable);
+    }
+
+    private void scheduleReconnect(WatchSubscription subscription) {
+        if (closed.get() || 
!subscription.reconnectScheduled.compareAndSet(false, true)) {
+            return;
+        }
+        try {
+            reconnectExecutor.schedule(() -> reconnect(subscription), 
RECONNECT_DELAY_MS,
+                                       TimeUnit.MILLISECONDS);
+        } catch (RuntimeException e) {
+            subscription.reconnectScheduled.set(false);
+            if (!closed.get()) {
+                log.warn("Failed to schedule watch reconnect for key {}", 
subscription.key, e);
+            }
+        }
+    }
+
+    private void reconnect(WatchSubscription subscription) {
+        subscription.reconnectScheduled.set(false);
+        if (closed.get()) {
+            return;
+        }
+        try {
+            if (!startWatch(subscription, false)) {
+                scheduleReconnect(subscription);
+            }
+        } catch (PDException e) {

Review Comment:
   ⚠️ Important. After peer retries are exhausted, 
`AbstractClient.streamingCall()` wraps the final gRPC status as `PDException`. 
`reconnect()` catches every `PDException` and schedules another attempt, so 
permanent statuses such as permission or authentication failures are retried 
forever without classification; `NON_RETRYABLE_WATCH_ERRORS` only protects the 
async callback path. Preserve the gRPC status and stop/report non-retryable 
synchronous failures.



##########
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,
+                   Status.Code.INVALID_ARGUMENT,
+                   Status.Code.NOT_FOUND,
+                   Status.Code.ALREADY_EXISTS,
+                   Status.Code.PERMISSION_DENIED,
+                   Status.Code.FAILED_PRECONDITION,
+                   Status.Code.OUT_OF_RANGE,
+                   Status.Code.UNIMPLEMENTED,
+                   Status.Code.DATA_LOSS,
+                   Status.Code.UNAUTHENTICATED);
+
+    private final AtomicLong lockClientId = new AtomicLong(0);
+    private final AtomicLong watchClientId = new AtomicLong(0);
+    private final Semaphore lockSemaphore = new Semaphore(1);
+    private final Semaphore watchSemaphore = new Semaphore(1);
+    private final AtomicBoolean closed = new AtomicBoolean(false);
+    private final Set<WatchSubscription> subscriptions = 
ConcurrentHashMap.newKeySet();
+    private final ScheduledExecutorService reconnectExecutor;
 
     public KvClient(PDConfig pdConfig) {
+        this(pdConfig, Executors.newSingleThreadScheduledExecutor(runnable -> {

Review Comment:
   ⚠️ Important. This executor is created for every `KvClient`, but it is shut 
down only by `KvClient.close()`. `SchemaDriver.destroy()` clears its caches and 
singleton without closing the client that owns these reconnect tasks, so old 
watches can continue after destroy and invoke callbacks against destroyed 
state. Make the driver lifecycle close the client, or give the client a 
destroy-safe ownership contract, and cover destroy with an active-watch 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()

Review Comment:
   ⚠️ Important. `WatchRequest` is built after the subscription and semaphore 
are installed, but outside the `try` that releases them. A null key (or another 
unchecked builder failure) therefore leaves the observer registered and the 
permit held; a later valid `listen` can block indefinitely on the watch 
semaphore. Build the request inside the guarded cleanup path and add an 
invalid-input test that verifies no permit or subscription is leaked.



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

Review Comment:
   ⚠️ Important. `startWatch()` returns true immediately after 
`asyncServerStreamingCall()` returns, before any `Starting` frame or other 
readiness signal. If the connection is blackholed or the server never sends the 
first frame, the subscription remains installed and the semaphore stays held 
forever, so reconnect cannot make progress. Add a first-frame deadline/watchdog 
that cleans up and reschedules the subscription when readiness is not observed.



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