This is an automated email from the ASF dual-hosted git repository.

jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 3cf99bd9fa0 Fix IoTConsensus SyncLog retry handling (#18317)
3cf99bd9fa0 is described below

commit 3cf99bd9fa05f562dd8f852efffb0badbd46e60d
Author: Jiang Tian <[email protected]>
AuthorDate: Tue Jul 28 10:43:02 2026 +0800

    Fix IoTConsensus SyncLog retry handling (#18317)
    
    * return proper status when IoTV1 follower is interrupted
    
    * return proper status when IoTV1 follower is interrupted
    
    * fix(consensus): retry sync log on write process errors
---
 .../iotdb/consensus/i18n/IoTConsensusMessages.java |   3 +
 .../iotdb/consensus/i18n/IoTConsensusMessages.java |   3 +
 .../consensus/iot/IoTConsensusServerImpl.java      |  13 ++-
 .../apache/iotdb/consensus/iot/ReplicateTest.java  | 106 +++++++++++++++++++++
 .../org/apache/iotdb/commons/utils/RetryUtils.java |   1 +
 .../apache/iotdb/commons/utils/RetryUtilsTest.java |  46 +++++++++
 6 files changed, 171 insertions(+), 1 deletion(-)

diff --git 
a/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
 
b/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
index 9d9b2a4869d..24efec2dd60 100644
--- 
a/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
+++ 
b/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
@@ -256,6 +256,9 @@ public final class IoTConsensusMessages {
       "waiting target request timeout. current index: {}, target index: {}";
   public static final String CURRENT_WAITING_INTERRUPTED =
       "current waiting is interrupted. SyncIndex: {}. Exception: ";
+  public static final String
+      
MESSAGE_SYNC_LOG_REQUEST_WITH_SYNC_INDEX_ARG_WAS_INTERRUPTED_WHILE_WAITING_81B4ABB2
 =
+          "SyncLog request with SyncIndex %d was interrupted while waiting";
 
   // ===================== SyncStatus =====================
 
diff --git 
a/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
 
b/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
index 9fca07f7bde..810ae0a3761 100644
--- 
a/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
+++ 
b/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
@@ -254,6 +254,9 @@ public final class IoTConsensusMessages {
       "等待目标请求超时。当前 index:{},目标 index:{}";
   public static final String CURRENT_WAITING_INTERRUPTED =
       "当前等待被中断。SyncIndex:{}。异常:";
+  public static final String
+      
MESSAGE_SYNC_LOG_REQUEST_WITH_SYNC_INDEX_ARG_WAS_INTERRUPTED_WHILE_WAITING_81B4ABB2
 =
+          "等待期间 SyncLog 请求(SyncIndex %d)被中断";
 
   // ===================== SyncStatus =====================
 
diff --git 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
index 1d55489f7cb..430507a4048 100644
--- 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
+++ 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
@@ -1435,8 +1435,19 @@ public class IoTConsensusServerImpl {
           } catch (InterruptedException e) {
             logger.warn(
                 IoTConsensusMessages.CURRENT_WAITING_INTERRUPTED, 
request.getStartSyncIndex(), e);
+            requestCache.remove(request);
+            queueSortCondition.signalAll();
             Thread.currentThread().interrupt();
-            break;
+            return new TSStatus()
+                .setSubStatus(
+                    Collections.nCopies(
+                        request.getInsertNodes().size(),
+                        RpcUtils.getStatus(
+                            TSStatusCode.INTERNAL_SERVER_ERROR,
+                            String.format(
+                                IoTConsensusMessages
+                                    
.MESSAGE_SYNC_LOG_REQUEST_WITH_SYNC_INDEX_ARG_WAS_INTERRUPTED_WHILE_WAITING_81B4ABB2,
+                                request.getStartSyncIndex()))));
           }
         }
         long sortTime = System.nanoTime();
diff --git 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/ReplicateTest.java
 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/ReplicateTest.java
index ca96ffec485..e096f559773 100644
--- 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/ReplicateTest.java
+++ 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/ReplicateTest.java
@@ -21,16 +21,23 @@ package org.apache.iotdb.consensus.iot;
 
 import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.consensus.ConsensusGroupId;
 import org.apache.iotdb.commons.consensus.DataRegionId;
 import org.apache.iotdb.commons.exception.StartupException;
+import org.apache.iotdb.commons.request.IConsensusRequest;
 import org.apache.iotdb.consensus.ConsensusFactory;
 import org.apache.iotdb.consensus.common.ConsensusGroup;
 import org.apache.iotdb.consensus.common.Peer;
+import 
org.apache.iotdb.consensus.common.request.DeserializedBatchIndexedConsensusRequest;
+import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
 import org.apache.iotdb.consensus.config.ConsensusConfig;
+import org.apache.iotdb.consensus.config.IoTConsensusConfig;
 import org.apache.iotdb.consensus.exception.ConsensusException;
+import org.apache.iotdb.consensus.i18n.IoTConsensusMessages;
 import org.apache.iotdb.consensus.iot.util.TestEntry;
 import org.apache.iotdb.consensus.iot.util.TestStateMachine;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.ratis.util.FileUtils;
 import org.junit.After;
@@ -48,6 +55,9 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
 
 public class ReplicateTest {
@@ -313,6 +323,85 @@ public class ReplicateTest {
     }
   }
 
+  /**
+   * Verifies that a SyncLog request interrupted while waiting for its turn is 
rejected without
+   * being applied, and that the thread's interrupted status is preserved.
+   */
+  @Test
+  public void syncLogInterruptedWhileWaitingTest() throws Exception {
+    servers.get(0).createLocalPeer(group.getGroupId(), group.getPeers());
+    IoTConsensusServerImpl server = servers.get(0).getImpl(gid);
+    DeserializedBatchIndexedConsensusRequest request =
+        new DeserializedBatchIndexedConsensusRequest(1, 1, 1, 
peers.get(1).getNodeId(), 1);
+    request.add(
+        new IndexedConsensusRequest(
+            1, 1, Collections.singletonList(new TestEntry(1, peers.get(1)))));
+    AtomicReference<TSStatus> status = new AtomicReference<>();
+    AtomicBoolean interrupted = new AtomicBoolean();
+
+    Thread syncLogThread =
+        new Thread(
+            () -> {
+              Thread.currentThread().interrupt();
+              status.set(server.syncLog(peers.get(1).getNodeId(), request));
+              interrupted.set(Thread.currentThread().isInterrupted());
+            });
+    syncLogThread.start();
+    syncLogThread.join();
+
+    Assert.assertTrue(interrupted.get());
+    Assert.assertEquals(1, status.get().getSubStatusSize());
+    Assert.assertEquals(
+        TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode(),
+        status.get().getSubStatus().get(0).getCode());
+    Assert.assertEquals(
+        String.format(
+            IoTConsensusMessages
+                
.MESSAGE_SYNC_LOG_REQUEST_WITH_SYNC_INDEX_ARG_WAS_INTERRUPTED_WHILE_WAITING_81B4ABB2,
+            request.getStartSyncIndex()),
+        status.get().getSubStatus().get(0).getMessage());
+    Assert.assertTrue(stateMachines.get(0).getRequestSet().isEmpty());
+  }
+
+  /**
+   * Verifies that WRITE_PROCESS_ERROR returned by a follower SyncLog write is 
retried by the leader
+   * and the request is eventually applied after the transient failure 
disappears.
+   */
+  @Test
+  public void syncLogWriteProcessErrorTriggersLeaderRetryTest() throws 
Exception {
+    IoTConsensusConfig retryConfig =
+        IoTConsensusConfig.newBuilder()
+            .setReplication(
+                IoTConsensusConfig.Replication.newBuilder()
+                    .setMaxWaitingTimeForWaitBatchInMs(50)
+                    .setBasicRetryWaitTimeMs(10)
+                    .setMaxRetryWaitTimeMs(100)
+                    .build())
+            .build();
+    ConsensusConfig consensusConfig =
+        
ConsensusConfig.newBuilder().setIoTConsensusConfig(retryConfig).build();
+    servers.forEach(server -> server.reloadConsensusConfig(consensusConfig));
+
+    WriteProcessErrorOnceTestStateMachine failingStateMachine =
+        new WriteProcessErrorOnceTestStateMachine();
+    stateMachines.set(1, failingStateMachine);
+    for (IoTConsensus server : servers) {
+      server.createLocalPeer(group.getGroupId(), group.getPeers());
+    }
+
+    TestEntry entry = new TestEntry(1, peers.get(0));
+    Assert.assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(), servers.get(0).write(gid, 
entry).getCode());
+    long deadline = System.currentTimeMillis() + 5_000;
+    while (!failingStateMachine.getData().contains(entry)
+        && System.currentTimeMillis() < deadline) {
+      Thread.sleep(20);
+    }
+
+    Assert.assertTrue(failingStateMachine.getWriteAttempts() >= 2);
+    Assert.assertTrue(failingStateMachine.getData().contains(entry));
+  }
+
   @Test
   public void parsingAndConstructIDTest() throws Exception {
     logger.info("Start ParsingAndConstructIDTest");
@@ -347,4 +436,21 @@ public class ReplicateTest {
         peers.stream().map(Peer::getNodeId).collect(Collectors.toSet()),
         
iotServerImpl.getConfiguration().stream().map(Peer::getNodeId).collect(Collectors.toSet()));
   }
+
+  private static class WriteProcessErrorOnceTestStateMachine extends 
TestStateMachine {
+
+    private final AtomicInteger writeAttempts = new AtomicInteger();
+
+    @Override
+    public TSStatus write(IConsensusRequest request) {
+      if (writeAttempts.incrementAndGet() == 1) {
+        return new TSStatus(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode());
+      }
+      return super.write(request);
+    }
+
+    private int getWriteAttempts() {
+      return writeAttempts.get();
+    }
+  }
 }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/RetryUtils.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/RetryUtils.java
index b07a95045c7..be84a0b2e3c 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/RetryUtils.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/RetryUtils.java
@@ -40,6 +40,7 @@ public class RetryUtils {
     return statusCode == TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode()
         || statusCode == TSStatusCode.SYSTEM_READ_ONLY.getStatusCode()
         || statusCode == TSStatusCode.WRITE_PROCESS_REJECT.getStatusCode()
+        || statusCode == TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode()
         || statusCode == 
TSStatusCode.METADATA_LEASE_FENCED_RETRY_REQUIRED.getStatusCode()
         || statusCode == TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode();
   }
diff --git 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/utils/RetryUtilsTest.java
 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/utils/RetryUtilsTest.java
new file mode 100644
index 00000000000..0c7d1dee996
--- /dev/null
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/utils/RetryUtilsTest.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.commons.utils;
+
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RetryUtilsTest {
+
+  /**
+   * Verifies that transient write failures, including WRITE_PROCESS_ERROR 
returned for an
+   * IOException during SyncLog, are retried while successful writes are not.
+   */
+  @Test
+  public void testNeedRetryForWrite() {
+    Assert.assertTrue(
+        
RetryUtils.needRetryForWrite(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode()));
+    
Assert.assertTrue(RetryUtils.needRetryForWrite(TSStatusCode.SYSTEM_READ_ONLY.getStatusCode()));
+    Assert.assertTrue(
+        
RetryUtils.needRetryForWrite(TSStatusCode.WRITE_PROCESS_REJECT.getStatusCode()));
+    Assert.assertTrue(
+        
RetryUtils.needRetryForWrite(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode()));
+    Assert.assertTrue(
+        
RetryUtils.needRetryForWrite(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()));
+    
Assert.assertFalse(RetryUtils.needRetryForWrite(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+  }
+}

Reply via email to