Repository: alibaba/nacos (2025-06-22T13:49:57Z)

Issue:
DefaultPushFuture do not cancel when ConnectionBusyExeception throwed
![Image](https://github.com/user-attachments/assets/e59da210-85cd-42e6-9678-119da8e9291b)
如上图，nacos 在2.4.x的版本后，在使用connection长连接进行服务变更推送的时候，会判断连接是否ready，如果不ready，就会抛出来Connection Busy Exception，然后出现第一次Error日志；此时 defaultPushFuture没有取消，会在5秒钟之后自行超时，打印超时ERROR日志，被监控系统捕获引发第二次告警。
问题在于这里并没有超时事件的发生，因为请求根本没发出去，日志给人误解以为nacos推送性能不行总超时。


Discussion:

Patch:
diff --git a/api/src/main/java/com/alibaba/nacos/api/remote/DefaultRequestFuture.java b/api/src/main/java/com/alibaba/nacos/api/remote/DefaultRequestFuture.java
index e09fd829f83..7c28f62b436 100644
--- a/api/src/main/java/com/alibaba/nacos/api/remote/DefaultRequestFuture.java
+++ b/api/src/main/java/com/alibaba/nacos/api/remote/DefaultRequestFuture.java
@@ -48,7 +48,7 @@ public class DefaultRequestFuture implements RequestFuture {
     
     private ScheduledFuture timeoutFuture;
     
-    TimeoutInnerTrigger timeoutInnerTrigger;
+    FutureTrigger futureTrigger;
     
     /**
      * Getter method for property <tt>requestCallBack</tt>.
@@ -73,16 +73,16 @@ public DefaultRequestFuture(String connectionId, String requestId) {
     }
     
     public DefaultRequestFuture(String connectionId, String requestId, RequestCallBack requestCallBack,
-            TimeoutInnerTrigger timeoutInnerTrigger) {
+            FutureTrigger futureTrigger) {
         this.timeStamp = System.currentTimeMillis();
         this.requestCallBack = requestCallBack;
         this.requestId = requestId;
         this.connectionId = connectionId;
         if (requestCallBack != null) {
-            this.timeoutFuture = RpcScheduledExecutor.TIMEOUT_SCHEDULER
-                    .schedule(new TimeoutHandler(), requestCallBack.getTimeout(), TimeUnit.MILLISECONDS);
+            this.timeoutFuture = RpcScheduledExecutor.TIMEOUT_SCHEDULER.schedule(new TimeoutHandler(),
+                    requestCallBack.getTimeout(), TimeUnit.MILLISECONDS);
         }
-        this.timeoutInnerTrigger = timeoutInnerTrigger;
+        this.futureTrigger = futureTrigger;
     }
     
     public void setResponse(final Response response) {
@@ -161,8 +161,8 @@ public Response get(long timeout) throws TimeoutException, InterruptedException
         if (isDone) {
             return response;
         } else {
-            if (timeoutInnerTrigger != null) {
-                timeoutInnerTrigger.triggerOnTimeout();
+            if (timeoutFuture == null && futureTrigger != null) {
+                futureTrigger.triggerOnTimeout();
             }
             throw new TimeoutException(
                     "request timeout after " + timeout + " milliseconds, requestId=" + requestId + ", connectionId="
@@ -192,18 +192,35 @@ public void run() {
             setFailResult(new TimeoutException(
                     "Timeout After " + requestCallBack.getTimeout() + " milliseconds, requestId=" + requestId
                             + ", connectionId=" + connectionId));
-            if (timeoutInnerTrigger != null) {
-                timeoutInnerTrigger.triggerOnTimeout();
+            if (futureTrigger != null) {
+                futureTrigger.triggerOnTimeout();
             }
         }
     }
     
-    public interface TimeoutInnerTrigger {
+    /**
+     * Cleaning something while request has been failed, canceled, timeout.
+     */
+    public interface FutureTrigger {
+        
+        /**
+         * default trigger for {@link #triggerOnTimeout()} and {@link #triggerOnCancel()}.
+         */
+        void defaultTrigger();
         
         /**
          * triggered on timeout .
          */
-        void triggerOnTimeout();
+        default void triggerOnTimeout() {
+            defaultTrigger();
+        }
+        
+        /**
+         * triggered on cancel.
+         */
+        default void triggerOnCancel() {
+            defaultTrigger();
+        }
         
     }
     
@@ -215,4 +232,25 @@ public interface TimeoutInnerTrigger {
     public String getConnectionId() {
         return connectionId;
     }
+    
+    /**
+     * Cancel the request. It should be called in
+     * {@link com.alibaba.nacos.core.remote.grpc.GrpcConnection#sendRequestInner}
+     * NOTE: For sync requests(which without requestCallBack), the cancel operation is always invalid.
+     *
+     * @param mayInterruptIfRunning whether to interrupt the thread
+     */
+    public void cancel(boolean mayInterruptIfRunning) {
+        synchronized (this) {
+            notifyAll();
+        }
+        // cancel timeout task.
+        if (timeoutFuture != null && !timeoutFuture.isDone()) {
+            boolean cancel = timeoutFuture.cancel(mayInterruptIfRunning);
+            if (cancel && futureTrigger != null) {
+                futureTrigger.triggerOnCancel();
+            }
+        }
+        
+    }
 }
diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImpl.java b/config/src/main/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImpl.java
old mode 100644
new mode 100755
diff --git a/core/src/main/java/com/alibaba/nacos/core/remote/grpc/GrpcConnection.java b/core/src/main/java/com/alibaba/nacos/core/remote/grpc/GrpcConnection.java
index 5bcbde2d3f7..7d93054522f 100644
--- a/core/src/main/java/com/alibaba/nacos/core/remote/grpc/GrpcConnection.java
+++ b/core/src/main/java/com/alibaba/nacos/core/remote/grpc/GrpcConnection.java
@@ -149,7 +149,12 @@ private DefaultRequestFuture sendRequestInner(Request request, RequestCallBack c
                 callBack, () -> RpcAckCallbackSynchronizer.clearFuture(getMetaInfo().getConnectionId(), requestId));
         
         RpcAckCallbackSynchronizer.syncCallback(getMetaInfo().getConnectionId(), requestId, defaultPushFuture);
-        sendRequestNoAck(request);
+        try {
+            sendRequestNoAck(request);
+        } catch (NacosRuntimeException nacosRuntimeException) {
+            defaultPushFuture.cancel(true);
+            throw nacosRuntimeException;
+        }
         return defaultPushFuture;
     }
     


Test patch:
diff --git a/api/src/test/java/com/alibaba/nacos/api/remote/DefaultRequestFutureTest.java b/api/src/test/java/com/alibaba/nacos/api/remote/DefaultRequestFutureTest.java
index e8f778a6d7a..28df95cc171 100644
--- a/api/src/test/java/com/alibaba/nacos/api/remote/DefaultRequestFutureTest.java
+++ b/api/src/test/java/com/alibaba/nacos/api/remote/DefaultRequestFutureTest.java
@@ -143,7 +143,7 @@ void testSyncGetResponseFailureWithTimeout() throws InterruptedException, Timeou
     
     @Test
     void testSyncGetResponseSuccessByTriggerWithoutTimeout() throws InterruptedException {
-        MockTimeoutInnerTrigger trigger = new MockTimeoutInnerTrigger();
+        MockFutureTrigger trigger = new MockFutureTrigger();
         DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, null, trigger);
         new Thread(() -> {
             try {
@@ -163,7 +163,7 @@ void testSyncGetResponseSuccessByTriggerWithoutTimeout() throws InterruptedExcep
     
     @Test
     void testSyncGetResponseFailureByTriggerWithoutTimeout() throws InterruptedException {
-        MockTimeoutInnerTrigger trigger = new MockTimeoutInnerTrigger();
+        MockFutureTrigger trigger = new MockFutureTrigger();
         DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, null, trigger);
         new Thread(() -> {
             try {
@@ -183,7 +183,7 @@ void testSyncGetResponseFailureByTriggerWithoutTimeout() throws InterruptedExcep
     
     @Test
     void testSyncGetResponseSuccessByTriggerWithTimeout() throws InterruptedException, TimeoutException {
-        MockTimeoutInnerTrigger trigger = new MockTimeoutInnerTrigger();
+        MockFutureTrigger trigger = new MockFutureTrigger();
         DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, null, trigger);
         new Thread(() -> {
             try {
@@ -199,12 +199,13 @@ void testSyncGetResponseSuccessByTriggerWithTimeout() throws InterruptedExceptio
         assertTrue(requestFuture.isDone());
         assertTrue(requestFuture.getTimeStamp() >= timestamp);
         assertFalse(trigger.isTimeout);
+        assertFalse(trigger.isCancel);
     }
     
     @Test
     void testSyncGetResponseFailureByTriggerWithTimeout() throws InterruptedException, TimeoutException {
         assertThrows(TimeoutException.class, () -> {
-            MockTimeoutInnerTrigger trigger = new MockTimeoutInnerTrigger();
+            MockFutureTrigger trigger = new MockFutureTrigger();
             DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, null, trigger);
             try {
                 requestFuture.get(100L);
@@ -216,7 +217,7 @@ void testSyncGetResponseFailureByTriggerWithTimeout() throws InterruptedExceptio
     
     @Test
     void testASyncGetResponseSuccessWithoutTimeout() throws InterruptedException {
-        MockTimeoutInnerTrigger trigger = new MockTimeoutInnerTrigger();
+        MockFutureTrigger trigger = new MockFutureTrigger();
         MockRequestCallback callback = new MockRequestCallback(200L);
         DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, callback, trigger);
         new Thread(() -> {
@@ -230,12 +231,13 @@ void testASyncGetResponseSuccessWithoutTimeout() throws InterruptedException {
         assertEquals(response, callback.response);
         assertNull(callback.exception);
         assertFalse(trigger.isTimeout);
+        assertFalse(trigger.isTimeout);
         assertEquals(callback, requestFuture.getRequestCallBack());
     }
     
     @Test
     void testASyncGetResponseSuccessWithoutTimeoutByExecutor() throws InterruptedException {
-        MockTimeoutInnerTrigger trigger = new MockTimeoutInnerTrigger();
+        MockFutureTrigger trigger = new MockFutureTrigger();
         MockRequestCallback callback = new MockRequestCallback(executor, 200L);
         DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, callback, trigger);
         new Thread(() -> {
@@ -252,7 +254,7 @@ void testASyncGetResponseSuccessWithoutTimeoutByExecutor() throws InterruptedExc
     
     @Test
     void testASyncGetResponseFailureWithoutTimeout() throws InterruptedException {
-        MockTimeoutInnerTrigger trigger = new MockTimeoutInnerTrigger();
+        MockFutureTrigger trigger = new MockFutureTrigger();
         MockRequestCallback callback = new MockRequestCallback(1000L);
         DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, callback, trigger);
         new Thread(() -> {
@@ -271,7 +273,7 @@ void testASyncGetResponseFailureWithoutTimeout() throws InterruptedException {
     
     @Test
     void testASyncGetResponseFailureWithTimeout() throws InterruptedException {
-        MockTimeoutInnerTrigger trigger = new MockTimeoutInnerTrigger();
+        MockFutureTrigger trigger = new MockFutureTrigger();
         MockRequestCallback callback = new MockRequestCallback(100L);
         final DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, callback,
                 trigger);
@@ -282,14 +284,75 @@ void testASyncGetResponseFailureWithTimeout() throws InterruptedException {
         assertEquals(callback, requestFuture.getRequestCallBack());
     }
     
-    private class MockTimeoutInnerTrigger implements DefaultRequestFuture.TimeoutInnerTrigger {
+    @Test
+    void testSyncRequestFutureCancelFailedWithTimeout() throws InterruptedException {
+        MockFutureTrigger trigger = new MockFutureTrigger();
+        final DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, null, trigger);
+        assertThrows(TimeoutException.class, () -> requestFuture.get(100L));
+        requestFuture.cancel(true);
+        assertTrue(trigger.isTimeout);
+        assertFalse(trigger.isCancel);
+    }
+    
+    @Test
+    void testSyncRequestFutureCancelFailed() throws InterruptedException {
+        MockFutureTrigger trigger = new MockFutureTrigger();
+        final DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, null, trigger);
+        requestFuture.cancel(true);
+        assertFalse(trigger.isTimeout);
+        assertFalse(trigger.isCancel);
+    }
+    
+    @Test
+    void testASyncRequestFutureCancelFailedWithTrigger() throws InterruptedException {
+        MockFutureTrigger trigger = new MockFutureTrigger();
+        MockRequestCallback callback = new MockRequestCallback(100L);
+        final DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, callback,
+                trigger);
+        TimeUnit.MILLISECONDS.sleep(500L);
+        requestFuture.cancel(true);
+        assertNull(callback.response);
+        assertTrue(callback.exception instanceof TimeoutException);
+        assertTrue(trigger.isTimeout);
+        assertFalse(trigger.isCancel);
+        assertEquals(callback, requestFuture.getRequestCallBack());
+    }
+    
+    @Test
+    void testASyncRequestFutureCancelSuccessWithTrigger() throws InterruptedException {
+        MockFutureTrigger trigger = new MockFutureTrigger();
+        MockRequestCallback callback = new MockRequestCallback(500L);
+        final DefaultRequestFuture requestFuture = new DefaultRequestFuture(CONNECTION_ID, REQUEST_ID, callback,
+                trigger);
+        TimeUnit.MILLISECONDS.sleep(100L);
+        requestFuture.cancel(true);
+        assertNull(callback.response);
+        assertNull(callback.exception);
+        assertFalse(trigger.isTimeout);
+        assertTrue(trigger.isCancel);
+        assertEquals(callback, requestFuture.getRequestCallBack());
+    }
+    
+    private class MockFutureTrigger implements DefaultRequestFuture.FutureTrigger {
         
         boolean isTimeout;
         
+        boolean isCancel;
+        
+        @Override
+        public void defaultTrigger() {
+            // do nothing
+        }
+        
         @Override
         public void triggerOnTimeout() {
             isTimeout = true;
         }
+        
+        @Override
+        public void triggerOnCancel() {
+            isCancel = true;
+        }
     }
     
     private class MockRequestCallback implements RequestCallBack<Response> {
@@ -331,4 +394,4 @@ public void onException(Throwable e) {
             exception = e;
         }
     }
-}
\ No newline at end of file
+}