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

Caideyipi pushed a commit to branch fix/pipe-cluster-replica-restart-barrier
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit 7642e305e57b7e79d04afbf7ac1838eb6108a900
Author: Caideyipi <[email protected]>
AuthorDate: Tue Oct 13 11:27:32 2026 +0800

    Fix queued IoTConsensus writes during shutdown
---
 .../manual/enhanced/IoTDBPipeClusterIT.java        | 38 +++++----
 .../auto/enhanced/IoTDBPipeClusterIT.java          | 38 +++++----
 .../consensus/iot/IoTConsensusServerImpl.java      |  8 +-
 .../consensus/iot/IoTConsensusServerImplTest.java  | 94 ++++++++++++++++++++++
 4 files changed, 145 insertions(+), 33 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/tablemodel/manual/enhanced/IoTDBPipeClusterIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/tablemodel/manual/enhanced/IoTDBPipeClusterIT.java
index a5e505f4fd7..20793825f7d 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/tablemodel/manual/enhanced/IoTDBPipeClusterIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/tablemodel/manual/enhanced/IoTDBPipeClusterIT.java
@@ -449,6 +449,8 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeTableModelDualManualIT {
 
   private void waitForTableDataRegionReplicationComplete(
       final BaseEnv env, final List<String> databases) {
+    // IoTConsensus accepts writes on any replica, including a previous leader 
during failover.
+    // Check every replica's outgoing replication before flushing or 
restarting the cluster.
     await()
         .pollInterval(500, TimeUnit.MILLISECONDS)
         .atMost(2, TimeUnit.MINUTES)
@@ -456,24 +458,23 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeTableModelDualManualIT {
             () -> {
               try (final SyncConfigNodeIServiceClient client =
                   (SyncConfigNodeIServiceClient) 
env.getLeaderConfigNodeConnection()) {
-                final List<TRegionInfo> leaderRegionInfoList =
-                    showTableDataRegionLeaders(databases, client);
+                final List<TRegionInfo> regionInfoList = 
showTableDataRegions(databases, client);
                 Assert.assertFalse(
-                    "No table DataRegion leader found for databases " + 
databases,
-                    leaderRegionInfoList.isEmpty());
+                    "No table DataRegion found for databases " + databases,
+                    regionInfoList.isEmpty());
 
-                for (final TRegionInfo regionInfo : leaderRegionInfoList) {
-                  final DataNodeWrapper leaderNode =
+                for (final TRegionInfo regionInfo : regionInfoList) {
+                  final DataNodeWrapper replicaNode =
                       findDataNodeWrapperByPort(env, 
regionInfo.getClientRpcPort());
                   final String metricsUrl =
                       "http://";
-                          + leaderNode.getIp()
+                          + replicaNode.getIp()
                           + ":"
-                          + leaderNode.getMetricPort()
+                          + replicaNode.getMetricPort()
                           + "/metrics";
                   final String metricsContent = env.getUrlContent(metricsUrl, 
null);
                   Assert.assertNotNull(
-                      "Failed to fetch metrics from leader DataNode at " + 
metricsUrl,
+                      "Failed to fetch metrics from replica DataNode at " + 
metricsUrl,
                       metricsContent);
                   assertSyncLagIsZero(metricsContent, 
buildDataRegionTag(regionInfo), metricsUrl);
                 }
@@ -483,6 +484,17 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeTableModelDualManualIT {
 
   private List<TRegionInfo> showTableDataRegionLeaders(
       final List<String> databases, final SyncConfigNodeIServiceClient client) 
throws TException {
+    final List<TRegionInfo> result = new ArrayList<>();
+    for (final TRegionInfo regionInfo : showTableDataRegions(databases, 
client)) {
+      if 
(RegionRoleType.Leader.getRoleType().equals(regionInfo.getRoleType())) {
+        result.add(regionInfo);
+      }
+    }
+    return result;
+  }
+
+  private List<TRegionInfo> showTableDataRegions(
+      final List<String> databases, final SyncConfigNodeIServiceClient client) 
throws TException {
     final TShowRegionResp showRegionResp =
         client.showRegion(
             new TShowRegionReq()
@@ -491,13 +503,7 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeTableModelDualManualIT {
                 .setIsTableModel(true));
     Assert.assertEquals(
         TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
showRegionResp.getStatus().getCode());
-    final List<TRegionInfo> result = new ArrayList<>();
-    for (final TRegionInfo regionInfo : showRegionResp.getRegionInfoList()) {
-      if 
(RegionRoleType.Leader.getRoleType().equals(regionInfo.getRoleType())) {
-        result.add(regionInfo);
-      }
-    }
-    return result;
+    return showRegionResp.getRegionInfoList();
   }
 
   private DataNodeWrapper findDataNodeWrapperByPort(final BaseEnv env, final 
int port) {
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/enhanced/IoTDBPipeClusterIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/enhanced/IoTDBPipeClusterIT.java
index 529147a77b0..b64a43fc04d 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/enhanced/IoTDBPipeClusterIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/enhanced/IoTDBPipeClusterIT.java
@@ -458,6 +458,8 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeDualTreeModelAutoIT {
 
   private void waitForTreeDataRegionReplicationComplete(
       final BaseEnv env, final List<String> databases) {
+    // IoTConsensus accepts writes on any replica, including a previous leader 
during failover.
+    // Check every replica's outgoing replication before flushing or 
restarting the cluster.
     await()
         .pollInterval(500, TimeUnit.MILLISECONDS)
         .atMost(2, TimeUnit.MINUTES)
@@ -465,24 +467,23 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeDualTreeModelAutoIT {
             () -> {
               try (final SyncConfigNodeIServiceClient client =
                   (SyncConfigNodeIServiceClient) 
env.getLeaderConfigNodeConnection()) {
-                final List<TRegionInfo> leaderRegionInfoList =
-                    showTreeDataRegionLeaders(databases, client);
+                final List<TRegionInfo> regionInfoList = 
showTreeDataRegions(databases, client);
                 Assert.assertFalse(
-                    "No tree DataRegion leader found for databases " + 
databases,
-                    leaderRegionInfoList.isEmpty());
+                    "No tree DataRegion found for databases " + databases,
+                    regionInfoList.isEmpty());
 
-                for (final TRegionInfo regionInfo : leaderRegionInfoList) {
-                  final DataNodeWrapper leaderNode =
+                for (final TRegionInfo regionInfo : regionInfoList) {
+                  final DataNodeWrapper replicaNode =
                       findDataNodeWrapperByPort(env, 
regionInfo.getClientRpcPort());
                   final String metricsUrl =
                       "http://";
-                          + leaderNode.getIp()
+                          + replicaNode.getIp()
                           + ":"
-                          + leaderNode.getMetricPort()
+                          + replicaNode.getMetricPort()
                           + "/metrics";
                   final String metricsContent = env.getUrlContent(metricsUrl, 
null);
                   Assert.assertNotNull(
-                      "Failed to fetch metrics from leader DataNode at " + 
metricsUrl,
+                      "Failed to fetch metrics from replica DataNode at " + 
metricsUrl,
                       metricsContent);
                   assertSyncLagIsZero(metricsContent, 
buildDataRegionTag(regionInfo), metricsUrl);
                 }
@@ -492,6 +493,17 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeDualTreeModelAutoIT {
 
   private List<TRegionInfo> showTreeDataRegionLeaders(
       final List<String> databases, final SyncConfigNodeIServiceClient client) 
throws TException {
+    final List<TRegionInfo> result = new ArrayList<>();
+    for (final TRegionInfo regionInfo : showTreeDataRegions(databases, 
client)) {
+      if 
(RegionRoleType.Leader.getRoleType().equals(regionInfo.getRoleType())) {
+        result.add(regionInfo);
+      }
+    }
+    return result;
+  }
+
+  private List<TRegionInfo> showTreeDataRegions(
+      final List<String> databases, final SyncConfigNodeIServiceClient client) 
throws TException {
     final TShowRegionResp showRegionResp =
         client.showRegion(
             new TShowRegionReq()
@@ -499,13 +511,7 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeDualTreeModelAutoIT {
                 .setDatabases(databases));
     Assert.assertEquals(
         TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
showRegionResp.getStatus().getCode());
-    final List<TRegionInfo> result = new ArrayList<>();
-    for (final TRegionInfo regionInfo : showRegionResp.getRegionInfoList()) {
-      if 
(RegionRoleType.Leader.getRoleType().equals(regionInfo.getRoleType())) {
-        result.add(regionInfo);
-      }
-    }
-    return result;
+    return showRegionResp.getRegionInfoList();
   }
 
   private DataNodeWrapper findDataNodeWrapperByPort(final BaseEnv env, final 
int port) {
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 ba4ad87b1fd..b61a4979618 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
@@ -1583,7 +1583,13 @@ public class IoTConsensusServerImpl {
         for (int i = 0; i < insertNodes.size(); i++) {
           IConsensusRequest insertNode = insertNodes.get(i);
           insertNode.markAsGeneratedByRemoteConsensusLeader();
-          TSStatus status = stateMachine.write(insertNode);
+          // The replica may become read-only while this batch waits for its 
turn. Recheck before
+          // applying each request so shutdown cannot acknowledge writes after 
the final flush.
+          TSStatus status =
+              isReadOnly()
+                  ? RpcUtils.getStatus(
+                      TSStatusCode.SYSTEM_READ_ONLY, 
ConsensusMessages.SYNC_LOG_SYSTEM_READ_ONLY)
+                  : stateMachine.write(insertNode);
           subStatus.add(status);
           if (RetryUtils.needRetryForWrite(status.getCode())) {
             for (int j = i + 1; j < insertNodes.size(); j++) {
diff --git 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImplTest.java
 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImplTest.java
index cc33cf1573a..291a8d99d1b 100644
--- 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImplTest.java
+++ 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImplTest.java
@@ -20,15 +20,21 @@
 package org.apache.iotdb.consensus.iot;
 
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.audit.UserDataTransferAuditHandler;
 import org.apache.iotdb.commons.consensus.ConsensusGroupId;
 import org.apache.iotdb.commons.consensus.DataRegionId;
 import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType;
 import org.apache.iotdb.commons.request.IConsensusRequest;
 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.IoTConsensusConfig;
 import org.apache.iotdb.consensus.config.UserDataTransferAuditClassifier;
+import org.apache.iotdb.consensus.i18n.ConsensusMessages;
+import org.apache.iotdb.consensus.iot.util.TestEntry;
 import org.apache.iotdb.consensus.iot.util.TestStateMachine;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -46,6 +52,8 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -59,6 +67,92 @@ public class IoTConsensusServerImplTest {
 
   @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
 
+  @Test
+  public void testSyncLogBecomesReadOnlyWhileWaiting() throws Exception {
+    final ScheduledExecutorService backgroundTaskService =
+        Executors.newSingleThreadScheduledExecutor();
+    final AtomicBoolean readOnly = new AtomicBoolean();
+    final TestStateMachine stateMachine =
+        new TestStateMachine() {
+          @Override
+          public boolean isReadOnly() {
+            return readOnly.get();
+          }
+        };
+    final Peer thisNode = new Peer(new DataRegionId(1), 0, new 
TEndPoint("127.0.0.1", 6667));
+    final IoTConsensusServerImpl server =
+        new IoTConsensusServerImpl(
+            temporaryFolder.getRoot().getAbsolutePath(),
+            null,
+            DirectoryStrategyType.SEQUENCE_STRATEGY,
+            thisNode,
+            Collections.singletonList(thisNode),
+            stateMachine,
+            backgroundTaskService,
+            null,
+            null,
+            IoTConsensusConfig.newBuilder()
+                .setReplication(
+                    IoTConsensusConfig.Replication.newBuilder()
+                        .setMaxWaitingTimeForWaitBatchInMs(100)
+                        .build())
+                .build());
+    final DeserializedBatchIndexedConsensusRequest request =
+        new DeserializedBatchIndexedConsensusRequest(1, 2, 2, 1, 2);
+    for (int index = 1; index <= 2; index++) {
+      request.add(
+          new IndexedConsensusRequest(
+              index, index, Collections.singletonList(new TestEntry(index, 
thisNode))));
+    }
+    final AtomicReference<TSStatus> status = new AtomicReference<>();
+    final Thread syncLogThread = new Thread(() -> status.set(server.syncLog(1, 
request)));
+    try {
+      assertFalse(server.isReadOnly());
+      syncLogThread.start();
+      final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
+      while (syncLogThread.getState() != Thread.State.TIMED_WAITING
+          && System.nanoTime() < deadline) {
+        Thread.sleep(10);
+      }
+      assertEquals(Thread.State.TIMED_WAITING, syncLogThread.getState());
+      readOnly.set(true);
+      syncLogThread.join(TimeUnit.SECONDS.toMillis(15));
+      assertFalse(syncLogThread.isAlive());
+      assertEquals(2, status.get().getSubStatusSize());
+      assertEquals(
+          TSStatusCode.SYSTEM_READ_ONLY.getStatusCode(),
+          status.get().getSubStatus().get(0).getCode());
+      assertEquals(
+          ConsensusMessages.SYNC_LOG_SYSTEM_READ_ONLY,
+          status.get().getSubStatus().get(0).getMessage());
+      assertEquals(
+          TSStatusCode.WRITE_PROCESS_REJECT.getStatusCode(),
+          status.get().getSubStatus().get(1).getCode());
+      assertTrue(stateMachine.getRequestSet().isEmpty());
+      assertEquals(0, server.getEffectiveSafePhysicalTime(1));
+
+      // The sender must be able to retry the complete batch after the replica 
becomes writable.
+      readOnly.set(false);
+      server.reloadConsensusConfig(
+          IoTConsensusConfig.newBuilder()
+              .setReplication(
+                  IoTConsensusConfig.Replication.newBuilder()
+                      .setMaxPendingBatchesNum(1)
+                      .setMaxWaitingTimeForWaitBatchInMs(100)
+                      .build())
+              .build());
+      final TSStatus retryStatus = server.syncLog(1, request);
+      assertTrue(
+          retryStatus.getSubStatus().stream()
+              .allMatch(s -> s.getCode() == 
TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+      assertEquals(2, stateMachine.getRequestSet().size());
+    } finally {
+      syncLogThread.interrupt();
+      syncLogThread.join(TimeUnit.SECONDS.toMillis(5));
+      backgroundTaskService.shutdownNow();
+    }
+  }
+
   @Test
   public void testSnapshotTransferAppliesGroupClassifier() {
     final DataRegionId groupId = new DataRegionId(1);

Reply via email to