Aias00 commented on code in PR #7727:
URL: https://github.com/apache/shenyu/pull/7727#discussion_r4303514356


##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListener.java:
##########
@@ -410,49 +465,159 @@ class LongPollingClient implements Runnable {
          */
         private Future<?> asyncTimeoutFuture;
 
+        /**
+         * The Heartbeat future.
+         */
+        private Future<?> heartbeatFuture;
+
+        /**
+         * Completed flag to prevent duplicate execution or responses.
+         */
+        private final AtomicBoolean isCompleted = new AtomicBoolean(false);
+
         /**
          * Instantiates a new Long polling client.
          *
          * @param ac          the ac
          * @param ip          the ip
          * @param timeoutTime the timeout time
+         * @param namespaceId the namespaceId
          */
         LongPollingClient(final AsyncContext ac, final String ip, final long 
timeoutTime, final String namespaceId) {
+            this(ac, ip, StringUtils.EMPTY, StringUtils.EMPTY, timeoutTime, 
namespaceId);
+        }
+
+        /**
+         * Instantiates a new Long polling client with port and bootstrap info.
+         *
+         * @param ac            the ac
+         * @param ip            the ip
+         * @param port          the port
+         * @param bootstrapInfo the bootstrap info
+         * @param timeoutTime   the timeout time
+         * @param namespaceId   the namespaceId
+         */
+        LongPollingClient(final AsyncContext ac, final String ip, final String 
port,
+                          final String bootstrapInfo, final long timeoutTime, 
final String namespaceId) {
             this.asyncContext = ac;
             this.ip = ip;
+            this.port = port;
+            this.bootstrapInfo = bootstrapInfo;
             this.timeoutTime = timeoutTime;
             this.namespaceId = namespaceId;
+            if (Objects.nonNull(ac)) {
+                try {
+                    ac.addListener(this);
+                } catch (Exception ex) {
+                    log.warn("failed to add async listener", ex);
+                }
+            }
         }
 
         @Override
         public void run() {
             try {
-                BlockingQueue<LongPollingClient> namespaceClients = 
clientsMap.getOrDefault(namespaceId, new ArrayBlockingQueue<>(1024));
+                BlockingQueue<LongPollingClient> namespaceClients = 
clientsMap.computeIfAbsent(
+                        namespaceId, k -> new ArrayBlockingQueue<>(1024));
                 this.asyncTimeoutFuture = scheduler.schedule(() -> {
-                    namespaceClients.remove(LongPollingClient.this);
                     List<ConfigGroupEnum> changedGroups = 
compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
                     sendResponse(changedGroups);
                     log.debug("LongPollingClient {} ", 
GsonUtils.getInstance().toJson(changedGroups));
                 }, timeoutTime, TimeUnit.MILLISECONDS);
+                if (shouldReportInstance(port)) {
+                    this.heartbeatFuture = scheduler.scheduleWithFixedDelay(
+                            this::refreshLiveness,
+                            heartbeatIntervalMs,
+                            heartbeatIntervalMs,
+                            TimeUnit.MILLISECONDS);
+                }
                 namespaceClients.add(this);
-                clientsMap.put(namespaceId, namespaceClients);
             } catch (Exception ex) {
                 log.error("add long polling client error", ex);
             }
         }
 
+        /**
+         * Refreshes the liveness of active held client.
+         */
+        void refreshLiveness() {
+            try {
+                if (isCompleted.get()) {
+                    return;
+                }
+                BlockingQueue<LongPollingClient> namespaceClients = 
clientsMap.get(namespaceId);
+                if (Objects.isNull(namespaceClients) || 
!namespaceClients.contains(this)) {
+                    cleanUp();
+                    return;
+                }
+                reportInstanceInfo(ip, port, bootstrapInfo, namespaceId);
+                log.debug("LongPollingClient refreshed liveness for ip={}, 
port={}, namespaceId={}", ip, port, namespaceId);
+            } catch (Exception e) {
+                log.error("refresh long polling client liveness error", e);
+            }
+        }
+
+        /**
+         * Cleans up futures and removes client from queue.
+         */
+        void cleanUp() {
+            if (Objects.nonNull(asyncTimeoutFuture)) {
+                asyncTimeoutFuture.cancel(false);
+            }
+            if (Objects.nonNull(heartbeatFuture)) {
+                heartbeatFuture.cancel(false);
+            }
+            BlockingQueue<LongPollingClient> namespaceClients = 
clientsMap.get(namespaceId);
+            if (Objects.nonNull(namespaceClients)) {
+                namespaceClients.remove(this);
+            }
+        }
+
         /**
          * Send response.
          *
          * @param changedGroups the changed groups
          */
         void sendResponse(final List<ConfigGroupEnum> changedGroups) {
-            // cancel scheduler
-            if (Objects.nonNull(asyncTimeoutFuture)) {
-                asyncTimeoutFuture.cancel(false);
+            if (!isCompleted.compareAndSet(false, true)) {

Review Comment:
   The old code removed the client from `clientsMap` unconditionally in the 
timeout task; removal now only happens via `cleanUp()` after this CAS succeeds. 
If `onError`/`onComplete` fired before `run()` added the client, `isCompleted` 
is already true, the listener's `cleanUp()` removed nothing, `run()` adds the 
client anyway, and this branch returns without cleaning up — the client then 
stays in the queue forever. Calling `cleanUp()` here (it is idempotent) or 
checking `isCompleted` at the top of `run()` closes it.



##########
shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListener.java:
##########
@@ -410,49 +465,159 @@ class LongPollingClient implements Runnable {
          */
         private Future<?> asyncTimeoutFuture;
 
+        /**
+         * The Heartbeat future.
+         */
+        private Future<?> heartbeatFuture;
+
+        /**
+         * Completed flag to prevent duplicate execution or responses.
+         */
+        private final AtomicBoolean isCompleted = new AtomicBoolean(false);
+
         /**
          * Instantiates a new Long polling client.
          *
          * @param ac          the ac
          * @param ip          the ip
          * @param timeoutTime the timeout time
+         * @param namespaceId the namespaceId
          */
         LongPollingClient(final AsyncContext ac, final String ip, final long 
timeoutTime, final String namespaceId) {
+            this(ac, ip, StringUtils.EMPTY, StringUtils.EMPTY, timeoutTime, 
namespaceId);
+        }
+
+        /**
+         * Instantiates a new Long polling client with port and bootstrap info.
+         *
+         * @param ac            the ac
+         * @param ip            the ip
+         * @param port          the port
+         * @param bootstrapInfo the bootstrap info
+         * @param timeoutTime   the timeout time
+         * @param namespaceId   the namespaceId
+         */
+        LongPollingClient(final AsyncContext ac, final String ip, final String 
port,
+                          final String bootstrapInfo, final long timeoutTime, 
final String namespaceId) {
             this.asyncContext = ac;
             this.ip = ip;
+            this.port = port;
+            this.bootstrapInfo = bootstrapInfo;
             this.timeoutTime = timeoutTime;
             this.namespaceId = namespaceId;
+            if (Objects.nonNull(ac)) {
+                try {
+                    ac.addListener(this);
+                } catch (Exception ex) {
+                    log.warn("failed to add async listener", ex);
+                }
+            }
         }
 
         @Override
         public void run() {
             try {
-                BlockingQueue<LongPollingClient> namespaceClients = 
clientsMap.getOrDefault(namespaceId, new ArrayBlockingQueue<>(1024));
+                BlockingQueue<LongPollingClient> namespaceClients = 
clientsMap.computeIfAbsent(
+                        namespaceId, k -> new ArrayBlockingQueue<>(1024));
                 this.asyncTimeoutFuture = scheduler.schedule(() -> {

Review Comment:
   Nit: `run()` schedules both futures and enqueues the client without checking 
`isCompleted`, which is what makes the race described on `sendResponse` 
possible. An early `if (isCompleted.get()) { cleanUp(); return; }` here would 
also avoid scheduling a heartbeat for a client that is already gone.



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

Reply via email to