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 532d6c13add Fence IoTConsensus applies before storage shutdown (#18841)
532d6c13add is described below

commit 532d6c13addc04946535729db388fb4c18feb6b9
Author: Caideyipi <[email protected]>
AuthorDate: Thu Oct 15 16:13:07 2026 +0800

    Fence IoTConsensus applies before storage shutdown (#18841)
    
    * Fix queued IoTConsensus writes during shutdown
    
    * Test queued IoTConsensus writes across storage shutdown
    
    * Reproduce WAL checkpoint loss from writes after shutdown
    
    * Fence IoTConsensus applies before storage shutdown
---
 .../manual/enhanced/IoTDBPipeClusterIT.java        |  41 +-
 .../auto/enhanced/IoTDBPipeClusterIT.java          |  38 +-
 .../consensus/iot/IoTConsensusServerImpl.java      |   9 +-
 .../consensus/iot/IoTConsensusServerImplTest.java  |  94 ++++
 .../dataregion/DataRegionStateMachine.java         |   6 +-
 .../IoTConsensusDataRegionStateMachine.java        |   7 +
 .../iotdb/db/service/DataNodeShutdownHook.java     |   3 +
 .../iotdb/db/storageengine/StorageEngine.java      |  13 +
 .../db/storageengine/dataregion/DataRegion.java    |  32 ++
 .../dataregion/IoTConsensusShutdownTest.java       | 593 +++++++++++++++++++++
 10 files changed, 802 insertions(+), 34 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..cde83cc4da6 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
@@ -348,6 +348,9 @@ public class IoTDBPipeClusterIT extends 
AbstractPipeTableModelDualManualIT {
 
         try (final SyncConfigNodeIServiceClient client =
             (SyncConfigNodeIServiceClient) 
senderEnv.getLeaderConfigNodeConnection()) {
+          // Distinguish a source-side write loss during the restart from a p2 
transfer failure.
+          TableModelUtils.assertData("test1", "test1", 0, 300, senderEnv);
+
           // Create a new pipe and write new data
           final Map<String, String> sourceAttributes = new HashMap<>();
           final Map<String, String> processorAttributes = new HashMap<>();
@@ -449,6 +452,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 +461,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 +487,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 +506,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 8e70866e379..ca29931f1c9 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
@@ -1617,7 +1617,14 @@ public class IoTConsensusServerImpl {
         for (int i = 0; i < insertNodes.size(); i++) {
           IConsensusRequest insertNode = insertNodes.get(i);
           insertNode.markAsGeneratedByRemoteConsensusLeader();
-          TSStatus status = stateMachine.write(insertNode);
+          // Reject requests that became read-only while queued. This is an 
early rejection only:
+          // the state machine must also fence the full apply against storage 
shutdown, since the
+          // replica can become read-only after this check.
+          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);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/DataRegionStateMachine.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/DataRegionStateMachine.java
index 3d2fb3d2006..127169ee2f8 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/DataRegionStateMachine.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/DataRegionStateMachine.java
@@ -251,7 +251,7 @@ public class DataRegionStateMachine extends 
BaseStateMachine {
     TSStatus result = null;
     int retryTime = 0;
     while (retryTime < MAX_WRITE_RETRY_TIMES) {
-      result = planNode.accept(new DataExecutionVisitor(), region);
+      result = executeWrite(planNode);
       // Let pipe retry with the original event instead of retrying a possibly 
mutated node here.
       if (needRetryForSpecificCases(result.getCode(), planNode)) {
         retryTime++;
@@ -284,6 +284,10 @@ public class DataRegionStateMachine extends 
BaseStateMachine {
     return result;
   }
 
+  protected TSStatus executeWrite(PlanNode planNode) {
+    return planNode.accept(new DataExecutionVisitor(), region);
+  }
+
   protected void waitBeforeNextWriteRetry() throws InterruptedException {
     Thread.sleep(WRITE_RETRY_WAIT_TIME_IN_MS);
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/IoTConsensusDataRegionStateMachine.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/IoTConsensusDataRegionStateMachine.java
index f3fecdf69f3..e9275f15587 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/IoTConsensusDataRegionStateMachine.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/IoTConsensusDataRegionStateMachine.java
@@ -50,6 +50,13 @@ public class IoTConsensusDataRegionStateMachine extends 
DataRegionStateMachine {
     super(region);
   }
 
+  @Override
+  protected TSStatus executeWrite(PlanNode planNode) {
+    final DataRegion targetRegion = region;
+    return targetRegion.executeIoTConsensusWrite(
+        () -> planNode.accept(new DataExecutionVisitor(), targetRegion));
+  }
+
   @Override
   public TSStatus write(IConsensusRequest request) {
     try {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java
index 828c4b811ac..cb1095da104 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java
@@ -120,6 +120,9 @@ public class DataNodeShutdownHook extends Thread {
     CommonDescriptor.getInstance()
         .getConfig()
         .setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.STOPPING);
+    // A pre-apply read-only check can race with this transition. Drain 
admitted IoTConsensus
+    // applies, including their WAL separators, and permanently reject new 
ones before flushing.
+    StorageEngine.getInstance().stopIoTConsensusWrites();
     // Wait all wal are flushed
     WALManager.getInstance().waitAllWALFlushed();
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java
index 72bdf195998..2f9c6efa6f4 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java
@@ -435,6 +435,7 @@ public class StorageEngine implements IService {
 
   @Override
   public void stop() {
+    stopIoTConsensusWrites();
     loadTsFileManager.stop();
     for (DataRegion dataRegion : dataRegionMap.values()) {
       if (dataRegion != null) {
@@ -455,6 +456,7 @@ public class StorageEngine implements IService {
 
   @Override
   public void shutdown(long milliseconds) throws ShutdownException {
+    stopIoTConsensusWrites();
     loadTsFileManager.stop();
     try {
       for (DataRegion dataRegion : dataRegionMap.values()) {
@@ -526,6 +528,17 @@ public class StorageEngine implements IService {
     dataRegionMap.clear();
   }
 
+  public void stopIoTConsensusWrites() {
+    if 
(!CONFIG.getDataRegionConsensusProtocolClass().equals(ConsensusFactory.IOT_CONSENSUS))
 {
+      return;
+    }
+    for (DataRegion dataRegion : dataRegionMap.values()) {
+      if (dataRegion != null) {
+        dataRegion.stopIoTConsensusWrites();
+      }
+    }
+  }
+
   /** flush command Sync asyncCloseOneProcessor all file node processors. */
   public void syncCloseAllProcessor() {
     LOGGER.info(StorageEngineMessages.START_CLOSING_ALL_DB_PROCESSOR);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java
index 46952ee87fa..1ac4d80b94f 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java
@@ -33,6 +33,7 @@ import 
org.apache.iotdb.commons.exception.DiskSpaceInsufficientException;
 import org.apache.iotdb.commons.exception.MetadataException;
 import 
org.apache.iotdb.commons.exception.MetadataLeaseFencedException.LeaseFencedRetryPolicy;
 import org.apache.iotdb.commons.file.SystemFileFactory;
+import org.apache.iotdb.commons.i18n.CommonMessages;
 import org.apache.iotdb.commons.path.IFullPath;
 import org.apache.iotdb.commons.path.MeasurementPath;
 import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
@@ -233,6 +234,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -285,6 +287,12 @@ public class DataRegion implements IDataRegionForQuery {
    */
   private final ReadWriteLock insertLock = new ReentrantReadWriteLock();
 
+  // Covers a complete IoTConsensus apply, including its WAL separator. Keep 
this separate from
+  // insertLock so memory backpressure and retry waits do not prevent flushing 
memtables.
+  private final ReadWriteLock ioTConsensusWriteLock = new 
ReentrantReadWriteLock();
+  // Accessed only under ioTConsensusWriteLock; stopping a region permanently 
closes admission.
+  private boolean ioTConsensusWritesStopped;
+
   /** Condition to safely delete data region. */
   private final Condition deletedCondition = 
insertLock.writeLock().newCondition();
 
@@ -2532,6 +2540,30 @@ public class DataRegion implements IDataRegionForQuery {
     WritingMetrics.getInstance().recordTimedFlushMemTableCount(count);
   }
 
+  public TSStatus executeIoTConsensusWrite(Supplier<TSStatus> write) {
+    ioTConsensusWriteLock.readLock().lock();
+    try {
+      if (ioTConsensusWritesStopped
+          || CommonDescriptor.getInstance().getConfig().isStopping()
+          || CommonDescriptor.getInstance().getConfig().isReadOnly()) {
+        return RpcUtils.getStatus(TSStatusCode.SYSTEM_READ_ONLY, 
CommonMessages.SYSTEM_READ_ONLY);
+      }
+      return write.get();
+    } finally {
+      ioTConsensusWriteLock.readLock().unlock();
+    }
+  }
+
+  /** Reject new IoTConsensus applies and drain admitted applies before 
closing storage or WAL. */
+  public void stopIoTConsensusWrites() {
+    ioTConsensusWriteLock.writeLock().lock();
+    try {
+      ioTConsensusWritesStopped = true;
+    } finally {
+      ioTConsensusWriteLock.writeLock().unlock();
+    }
+  }
+
   /** This method will be blocked until all tsfile processors are closed. */
   public void syncCloseAllWorkingTsFileProcessors() {
     try {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/consensus/statemachine/dataregion/IoTConsensusShutdownTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/consensus/statemachine/dataregion/IoTConsensusShutdownTest.java
new file mode 100644
index 00000000000..fed7a6151a1
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/consensus/statemachine/dataregion/IoTConsensusShutdownTest.java
@@ -0,0 +1,593 @@
+/*
+ * 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.db.consensus.statemachine.dataregion;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.cluster.NodeStatus;
+import org.apache.iotdb.commons.concurrent.ExceptionalCountDownLatch;
+import org.apache.iotdb.commons.conf.CommonConfig;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
+import org.apache.iotdb.commons.consensus.DataRegionId;
+import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType;
+import org.apache.iotdb.commons.path.PartialPath;
+import 
org.apache.iotdb.commons.queryengine.plan.planner.plan.node.IPlanVisitor;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.commons.request.IConsensusRequest;
+import org.apache.iotdb.consensus.ConsensusFactory;
+import org.apache.iotdb.consensus.common.Peer;
+import 
org.apache.iotdb.consensus.common.request.DeserializedBatchIndexedConsensusRequest;
+import org.apache.iotdb.consensus.config.IoTConsensusConfig;
+import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl;
+import org.apache.iotdb.db.conf.IoTDBConfig;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
+import org.apache.iotdb.db.storageengine.StorageEngine;
+import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
+import 
org.apache.iotdb.db.storageengine.dataregion.compaction.schedule.CompactionTaskManager;
+import org.apache.iotdb.db.storageengine.dataregion.flush.TsFileFlushPolicy;
+import org.apache.iotdb.db.storageengine.dataregion.memtable.TsFileProcessor;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import org.apache.iotdb.db.storageengine.dataregion.wal.WALManager;
+import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALBuffer;
+import org.apache.iotdb.db.storageengine.dataregion.wal.node.WALNode;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.recover.CheckpointRecoverUtils;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.recover.WALRecoverManager;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.recover.file.UnsealedTsFileRecoverPerformer;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALMode;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.utils.listener.WALRecoverListener;
+import org.apache.iotdb.db.utils.EnvironmentUtils;
+import org.apache.iotdb.db.utils.constant.TestConstant;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.TsFileReader;
+import org.apache.tsfile.read.TsFileSequenceReader;
+import org.apache.tsfile.read.common.Path;
+import org.apache.tsfile.read.common.RowRecord;
+import org.apache.tsfile.read.expression.QueryExpression;
+import org.apache.tsfile.read.query.dataset.QueryDataSet;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+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.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+
+public class IoTConsensusShutdownTest {
+
+  private static final String DATABASE = "root.shutdown";
+  private static final String DEVICE = DATABASE + ".d1";
+  private static final String MEASUREMENT = "s1";
+
+  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+  private final CommonConfig commonConfig = 
CommonDescriptor.getInstance().getConfig();
+  private WALMode previousWalMode;
+  private int previousDataNodeId;
+  private String previousConsensusProtocol;
+  private NodeStatus previousNodeStatus;
+  private String previousStatusReason;
+  private boolean previousStopping;
+  private DataRegion dataRegion;
+  private IoTConsensusDataRegionStateMachine stateMachine;
+
+  @Before
+  public void setUp() throws Exception {
+    previousWalMode = config.getWalMode();
+    previousDataNodeId = config.getDataNodeId();
+    previousConsensusProtocol = config.getDataRegionConsensusProtocolClass();
+    previousNodeStatus = commonConfig.getNodeStatus();
+    previousStatusReason = commonConfig.getStatusReason();
+    previousStopping = commonConfig.isStopping();
+    config.setWalMode(WALMode.ASYNC);
+    config.setDataNodeId(0);
+    config.setDataRegionConsensusProtocolClass(ConsensusFactory.IOT_CONSENSUS);
+    commonConfig.setNodeStatus(NodeStatus.Running);
+    commonConfig.setStopping(false);
+    EnvironmentUtils.envSetUp();
+    dataRegion =
+        new DataRegion(
+            TestConstant.OUTPUT_DATA_DIR + "shutdown-info",
+            "0",
+            new TsFileFlushPolicy.DirectFlushPolicy(),
+            DATABASE);
+    StorageEngine.getInstance().setDataRegion(new DataRegionId(0), dataRegion);
+    stateMachine = new IoTConsensusDataRegionStateMachine(dataRegion);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    try {
+      commonConfig.setStopping(false);
+      commonConfig.setNodeStatus(NodeStatus.Running);
+      if (dataRegion != null) {
+        dataRegion.syncCloseAllWorkingTsFileProcessors();
+      }
+      CompactionTaskManager.getInstance().stop();
+      EnvironmentUtils.cleanEnv();
+    } finally {
+      config.setWalMode(previousWalMode);
+      config.setDataNodeId(previousDataNodeId);
+      config.setDataRegionConsensusProtocolClass(previousConsensusProtocol);
+      commonConfig.setNodeStatus(NodeStatus.Running);
+      commonConfig.setNodeStatusWithReason(previousNodeStatus, 
previousStatusReason);
+      commonConfig.setStopping(previousStopping);
+    }
+  }
+
+  @Test
+  public void testQueuedSyncLogDoesNotReopenStorageAfterShutdown() throws 
Exception {
+    writeAndAssertSuccess(1);
+    final ScheduledExecutorService backgroundTaskService =
+        Executors.newSingleThreadScheduledExecutor();
+    final IoTConsensusServerImpl server = newServer(stateMachine, 
backgroundTaskService, 2);
+    final DeserializedBatchIndexedConsensusRequest request =
+        new DeserializedBatchIndexedConsensusRequest(1, 2, 2, 1, 2);
+    request.add(insertRow(2));
+    request.add(insertRow(3));
+    final AtomicReference<TSStatus> status = new AtomicReference<>();
+    final Thread syncLogThread = new Thread(() -> status.set(server.syncLog(1, 
request)));
+    try {
+      assertFalse(server.isReadOnly());
+      syncLogThread.start();
+      Awaitility.await()
+          .atMost(10, TimeUnit.SECONDS)
+          .until(() -> syncLogThread.getState() == Thread.State.TIMED_WAITING);
+
+      // Follow the production order: read-only, final flush, storage stop, 
then WAL stop.
+      commonConfig.setStopping(true);
+      commonConfig.setNodeStatus(NodeStatus.ReadOnly);
+      StorageEngine.getInstance().stopIoTConsensusWrites();
+      WALManager.getInstance().waitAllWALFlushed();
+      StorageEngine.getInstance().syncCloseAllProcessor();
+      final File flushedFile = 
dataRegion.getSequenceFileList().get(0).getTsFile();
+      StorageEngine.getInstance().stop();
+      WALManager.getInstance().stop();
+      assertEquals(0, WALManager.getInstance().getWALNodesNum());
+      assertTrue(syncLogThread.isAlive());
+
+      // An empty preceding batch wakes the ordered request without another 
storage write.
+      server.syncLog(1, new DeserializedBatchIndexedConsensusRequest(0, 0, 0, 
1, 0));
+      syncLogThread.join(TimeUnit.SECONDS.toMillis(10));
+      assertFalse(syncLogThread.isAlive());
+      assertEquals(2, status.get().getSubStatusSize());
+      assertEquals(
+          status.get()
+              + ", WAL nodes: "
+              + WALManager.getInstance().getWALNodesNum()
+              + ", working TsFile processors: "
+              + dataRegion.getWorkSequenceTsFileProcessors().size(),
+          TSStatusCode.SYSTEM_READ_ONLY.getStatusCode(),
+          status.get().getSubStatus().get(0).getCode());
+      assertEquals(
+          TSStatusCode.WRITE_PROCESS_REJECT.getStatusCode(),
+          status.get().getSubStatus().get(1).getCode());
+      assertTrue(dataRegion.getWorkSequenceTsFileProcessors().isEmpty());
+      assertTrue(dataRegion.getWorkUnsequenceTsFileProcessors().isEmpty());
+      assertEquals(0, WALManager.getInstance().getWALNodesNum());
+      assertEquals(0, server.getEffectiveSafePhysicalTime(1));
+      assertSingleRow(flushedFile, 1);
+    } finally {
+      syncLogThread.interrupt();
+      syncLogThread.join(TimeUnit.SECONDS.toMillis(10));
+      backgroundTaskService.shutdownNow();
+    }
+  }
+
+  @Test
+  public void testSyncLogBecomesReadOnlyAfterPreApplyCheck() throws Exception {
+    writeAndAssertSuccess(1);
+    final CountDownLatch preApplyCheckPassed = new CountDownLatch(1);
+    final CountDownLatch resumeApply = new CountDownLatch(1);
+    final IoTConsensusDataRegionStateMachine delayedStateMachine = 
spy(stateMachine);
+    doAnswer(
+            invocation -> {
+              preApplyCheckPassed.countDown();
+              assertTrue(resumeApply.await(30, TimeUnit.SECONDS));
+              return invocation.callRealMethod();
+            })
+        .when(delayedStateMachine)
+        .write(any(IConsensusRequest.class));
+    final ScheduledExecutorService backgroundTaskService =
+        Executors.newSingleThreadScheduledExecutor();
+    final ExecutorService syncLogExecutor = 
Executors.newSingleThreadExecutor();
+    final IoTConsensusServerImpl server = newServer(delayedStateMachine, 
backgroundTaskService, 1);
+    final DeserializedBatchIndexedConsensusRequest request =
+        new DeserializedBatchIndexedConsensusRequest(1, 2, 2, 1, 2);
+    request.add(insertRow(2));
+    request.add(insertRow(3));
+    final Future<TSStatus> syncLog = syncLogExecutor.submit(() -> 
server.syncLog(1, request));
+    try {
+      // The consensus-layer read-only check has returned false, but apply has 
not started.
+      assertTrue(preApplyCheckPassed.await(10, TimeUnit.SECONDS));
+      commonConfig.setStopping(true);
+      commonConfig.setNodeStatus(NodeStatus.ReadOnly);
+      StorageEngine.getInstance().stopIoTConsensusWrites();
+      WALManager.getInstance().waitAllWALFlushed();
+      StorageEngine.getInstance().syncCloseAllProcessor();
+      final File flushedFile = 
dataRegion.getSequenceFileList().get(0).getTsFile();
+      StorageEngine.getInstance().stop();
+      WALManager.getInstance().stop();
+      assertEquals(0, WALManager.getInstance().getWALNodesNum());
+
+      resumeApply.countDown();
+      final TSStatus status = syncLog.get(10, TimeUnit.SECONDS);
+      assertEquals(2, status.getSubStatusSize());
+      assertEquals(
+          TSStatusCode.SYSTEM_READ_ONLY.getStatusCode(), 
status.getSubStatus().get(0).getCode());
+      assertEquals(
+          TSStatusCode.WRITE_PROCESS_REJECT.getStatusCode(),
+          status.getSubStatus().get(1).getCode());
+      assertEquals(0, server.getEffectiveSafePhysicalTime(1));
+      assertEquals(0, WALManager.getInstance().getWALNodesNum());
+      assertTrue(dataRegion.getWorkSequenceTsFileProcessors().isEmpty());
+      assertTrue(dataRegion.getWorkUnsequenceTsFileProcessors().isEmpty());
+      assertSingleRow(flushedFile, 1);
+    } finally {
+      resumeApply.countDown();
+      syncLogExecutor.shutdownNow();
+      assertTrue(syncLogExecutor.awaitTermination(10, TimeUnit.SECONDS));
+      backgroundTaskService.shutdownNow();
+    }
+  }
+
+  @Test
+  public void testShutdownDrainsAdmittedApplyBeforeClosingStorage() throws 
Exception {
+    writeAndAssertSuccess(1);
+    final CountDownLatch admittedApply = new CountDownLatch(1);
+    final CountDownLatch resumeApply = new CountDownLatch(1);
+    final InsertRowNode pendingRow = spy(insertRow(2));
+    doAnswer(
+            invocation -> {
+              admittedApply.countDown();
+              assertTrue(resumeApply.await(30, TimeUnit.SECONDS));
+              return invocation.callRealMethod();
+            })
+        .when(pendingRow)
+        .accept(any(IPlanVisitor.class), any(DataRegion.class));
+    final ExecutorService executor = Executors.newFixedThreadPool(2);
+    final CountDownLatch shutdownStarted = new CountDownLatch(1);
+    final CountDownLatch admissionClosed = new CountDownLatch(1);
+    final AtomicReference<Thread> shutdownThread = new AtomicReference<>();
+    final Future<TSStatus> write =
+        executor.submit(() -> stateMachine.write((IConsensusRequest) 
pendingRow));
+    try {
+      // Unlike the pre-apply-check test, this request has passed the check 
under the lifecycle
+      // lock.
+      assertTrue(admittedApply.await(10, TimeUnit.SECONDS));
+      commonConfig.setStopping(true);
+      commonConfig.setNodeStatus(NodeStatus.ReadOnly);
+      final Future<?> shutdown =
+          executor.submit(
+              () -> {
+                shutdownThread.set(Thread.currentThread());
+                shutdownStarted.countDown();
+                StorageEngine.getInstance().stopIoTConsensusWrites();
+                admissionClosed.countDown();
+                WALManager.getInstance().waitAllWALFlushed();
+                StorageEngine.getInstance().syncCloseAllProcessor();
+                StorageEngine.getInstance().stop();
+                WALManager.getInstance().stop();
+              });
+      assertTrue(shutdownStarted.await(10, TimeUnit.SECONDS));
+      Awaitility.await()
+          .atMost(10, TimeUnit.SECONDS)
+          .until(
+              () ->
+                  admissionClosed.getCount() == 0
+                      || shutdownThread.get().getState() == 
Thread.State.WAITING);
+      assertEquals(1, admissionClosed.getCount());
+      assertFalse(shutdown.isDone());
+      assertFalse(dataRegion.getSequenceFileList().get(0).isClosed());
+
+      resumeApply.countDown();
+      assertEquals(
+          TSStatusCode.SUCCESS_STATUS.getStatusCode(), write.get(10, 
TimeUnit.SECONDS).getCode());
+      shutdown.get(10, TimeUnit.SECONDS);
+      assertRows(dataRegion.getSequenceFileList().get(0).getTsFile(), 1, 2);
+      assertTrue(dataRegion.getWorkSequenceTsFileProcessors().isEmpty());
+      assertTrue(dataRegion.getWorkUnsequenceTsFileProcessors().isEmpty());
+      assertEquals(0, WALManager.getInstance().getWALNodesNum());
+
+      // Stopping storage closes admission permanently, even if the node 
status changes again.
+      commonConfig.setStopping(false);
+      commonConfig.setNodeStatus(NodeStatus.Running);
+      assertEquals(
+          TSStatusCode.SYSTEM_READ_ONLY.getStatusCode(),
+          stateMachine.write((IConsensusRequest) insertRow(3)).getCode());
+      assertEquals(0, WALManager.getInstance().getWALNodesNum());
+    } finally {
+      resumeApply.countDown();
+      executor.shutdownNow();
+      assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+    }
+  }
+
+  @Test
+  public void testShutdownDuringWriteRetryDoesNotHoldAdmissionLock() throws 
Exception {
+    writeAndAssertSuccess(1);
+    final CountDownLatch retryWaiting = new CountDownLatch(1);
+    final CountDownLatch resumeRetry = new CountDownLatch(1);
+    stateMachine =
+        new IoTConsensusDataRegionStateMachine(dataRegion) {
+          @Override
+          protected void waitBeforeNextWriteRetry() throws 
InterruptedException {
+            retryWaiting.countDown();
+            assertTrue(resumeRetry.await(30, TimeUnit.SECONDS));
+          }
+        };
+    final AtomicInteger applyAttempts = new AtomicInteger();
+    final InsertRowNode pendingRow = spy(insertRow(2));
+    doAnswer(
+            invocation ->
+                applyAttempts.incrementAndGet() == 1
+                    ? new 
TSStatus(TSStatusCode.WRITE_PROCESS_REJECT.getStatusCode())
+                    : invocation.callRealMethod())
+        .when(pendingRow)
+        .accept(any(IPlanVisitor.class), any(DataRegion.class));
+    final ExecutorService executor = Executors.newSingleThreadExecutor();
+    final Future<TSStatus> write =
+        executor.submit(() -> stateMachine.write((IConsensusRequest) 
pendingRow));
+    try {
+      assertTrue(retryWaiting.await(10, TimeUnit.SECONDS));
+      commonConfig.setStopping(true);
+      commonConfig.setNodeStatus(NodeStatus.ReadOnly);
+      StorageEngine.getInstance().stopIoTConsensusWrites();
+      WALManager.getInstance().waitAllWALFlushed();
+      StorageEngine.getInstance().syncCloseAllProcessor();
+      final File flushedFile = 
dataRegion.getSequenceFileList().get(0).getTsFile();
+      StorageEngine.getInstance().stop();
+      WALManager.getInstance().stop();
+      assertFalse(write.isDone());
+
+      resumeRetry.countDown();
+      assertEquals(
+          TSStatusCode.SYSTEM_READ_ONLY.getStatusCode(), write.get(10, 
TimeUnit.SECONDS).getCode());
+      assertEquals(1, applyAttempts.get());
+      assertTrue(dataRegion.getWorkSequenceTsFileProcessors().isEmpty());
+      assertTrue(dataRegion.getWorkUnsequenceTsFileProcessors().isEmpty());
+      assertEquals(0, WALManager.getInstance().getWALNodesNum());
+      assertSingleRow(flushedFile, 1);
+    } finally {
+      resumeRetry.countDown();
+      executor.shutdownNow();
+      assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+    }
+  }
+
+  @Test
+  public void testReadOnlyCanBecomeWritableBeforeShutdown() throws Exception {
+    writeAndAssertSuccess(1);
+    commonConfig.setNodeStatus(NodeStatus.ReadOnly);
+    assertEquals(
+        TSStatusCode.SYSTEM_READ_ONLY.getStatusCode(),
+        stateMachine.write((IConsensusRequest) insertRow(2)).getCode());
+    commonConfig.setNodeStatus(NodeStatus.Running);
+    writeAndAssertSuccess(2);
+    dataRegion.syncCloseAllWorkingTsFileProcessors();
+    assertRows(dataRegion.getSequenceFileList().get(0).getTsFile(), 1, 2);
+  }
+
+  private IoTConsensusServerImpl newServer(
+      IoTConsensusDataRegionStateMachine targetStateMachine,
+      ScheduledExecutorService backgroundTaskService,
+      int maxPendingBatchesNum)
+      throws Exception {
+    final Peer thisNode = new Peer(new DataRegionId(0), 0, new 
TEndPoint("127.0.0.1", 6667));
+    return new IoTConsensusServerImpl(
+        temporaryFolder.getRoot().getAbsolutePath(),
+        null,
+        DirectoryStrategyType.SEQUENCE_STRATEGY,
+        thisNode,
+        Collections.singletonList(thisNode),
+        targetStateMachine,
+        backgroundTaskService,
+        null,
+        null,
+        IoTConsensusConfig.newBuilder()
+            .setReplication(
+                IoTConsensusConfig.Replication.newBuilder()
+                    .setMaxPendingBatchesNum(maxPendingBatchesNum)
+                    
.setMaxWaitingTimeForWaitBatchInMs(TimeUnit.MINUTES.toMillis(5))
+                    .build())
+            .build());
+  }
+
+  @Test
+  public void testUncheckedWriteAfterWalManagerStopLosesCheckpoint() throws 
Exception {
+    writeAndAssertSuccess(1);
+    commonConfig.setNodeStatus(NodeStatus.ReadOnly);
+    WALManager.getInstance().waitAllWALFlushed();
+    StorageEngine.getInstance().syncCloseAllProcessor();
+    final File flushedFile = 
dataRegion.getSequenceFileList().get(0).getTsFile();
+    final WALNode originalWalNode = (WALNode) dataRegion.getWALNode().get();
+    StorageEngine.getInstance().stop();
+    WALManager.getInstance().stop();
+
+    // Bypass lifecycle admission with the real visitor to reproduce the old 
late-apply behavior.
+    // Unlike a write before WAL stop, this recreates the node in a directory 
containing closed WAL.
+    writeUncheckedAndAssertSuccess(2);
+    final WALNode lateWalNode = (WALNode) dataRegion.getWALNode().get();
+    assertNotSame(originalWalNode, lateWalNode);
+    assertEquals(originalWalNode.getLogDirectory(), 
lateWalNode.getLogDirectory());
+    final File lateFile =
+        dataRegion
+            .getWorkSequenceTsFileProcessors()
+            .iterator()
+            .next()
+            .getTsFileResource()
+            .getTsFile();
+    WALManager.getInstance().waitAllWALFlushed();
+    WALManager.getInstance().stop();
+    // The new CheckpointManager appends another header to the existing 
_0.checkpoint file. Even
+    // with a drained and normally closed WAL, recovery cannot find the late 
memtable in that file.
+    assertTrue(
+        
CheckpointRecoverUtils.recoverMemTableInfo(lateWalNode.getLogDirectory())
+            .getMemTableId2Info()
+            .isEmpty());
+    dataRegion.forceCloseAllWorkingTsFileProcessors();
+    dataRegion.getWorkSequenceTsFileProcessors().clear();
+    dataRegion.getWorkUnsequenceTsFileProcessors().clear();
+
+    final WALRecoverManager recoverManager = WALRecoverManager.getInstance();
+    recoverManager.clear();
+    final UnsealedTsFileRecoverPerformer performer =
+        new UnsealedTsFileRecoverPerformer(
+            new TsFileResource(lateFile), true, recovered -> 
assertFalse(recovered.canWrite()));
+    recoverManager.addRecoverPerformer(performer);
+    recoverManager.setAllDataRegionScannedLatch(new 
ExceptionalCountDownLatch(0));
+    recoverManager.recover();
+    assertEquals(WALRecoverListener.Status.SUCCESS, 
performer.getRecoverListener().waitForResult());
+    assertSingleRow(flushedFile, 1);
+    assertTrue(performer.getTsFileResource().isEmpty());
+  }
+
+  @Test
+  public void testAsyncWriteAfterTsFileFlushIsRecoveredWhenWalCloses() throws 
Exception {
+    writeAndAssertSuccess(1);
+    commonConfig.setNodeStatus(NodeStatus.ReadOnly);
+    WALManager.getInstance().waitAllWALFlushed();
+    StorageEngine.getInstance().syncCloseAllProcessor();
+    assertTrue(dataRegion.getWorkSequenceTsFileProcessors().isEmpty());
+    final File flushedFile = 
dataRegion.getSequenceFileList().get(0).getTsFile();
+    final WALBuffer buffer = ((WALNode) 
dataRegion.getWALNode().get()).getWALBuffer();
+
+    // Hold the real WAL sync executor so the async ACK cannot imply that the 
WAL is durable.
+    final Field syncBufferThreadField = 
WALBuffer.class.getDeclaredField("syncBufferThread");
+    syncBufferThreadField.setAccessible(true);
+    final ExecutorService syncExecutor = (ExecutorService) 
syncBufferThreadField.get(buffer);
+    final CountDownLatch syncBlocked = new CountDownLatch(1);
+    final CountDownLatch resumeSync = new CountDownLatch(1);
+    final Future<?> blocker =
+        syncExecutor.submit(
+            () -> {
+              syncBlocked.countDown();
+              assertTrue(resumeSync.await(30, TimeUnit.SECONDS));
+              return null;
+            });
+    final File lateFile;
+    try {
+      assertTrue(syncBlocked.await(10, TimeUnit.SECONDS));
+      // Call the real visitor directly to model the old unchecked SyncLog 
apply.
+      writeUncheckedAndAssertSuccess(2);
+      assertFalse(buffer.isAllWALEntriesConsumed());
+      assertEquals(1, dataRegion.getWorkSequenceTsFileProcessors().size());
+      final TsFileProcessor processor =
+          dataRegion.getWorkSequenceTsFileProcessors().iterator().next();
+      assertEquals(1, processor.getWorkMemTable().getTotalPointsNum());
+      lateFile = processor.getTsFileResource().getTsFile();
+    } finally {
+      resumeSync.countDown();
+      blocker.get(10, TimeUnit.SECONDS);
+    }
+
+    // Normal WAL close drains the late write, even though there is no further 
TsFile flush.
+    WALManager.getInstance().stop();
+    dataRegion.forceCloseAllWorkingTsFileProcessors();
+    dataRegion.getWorkSequenceTsFileProcessors().clear();
+    dataRegion.getWorkUnsequenceTsFileProcessors().clear();
+    assertFalse(new File(lateFile.getPath() + 
TsFileResource.RESOURCE_SUFFIX).exists());
+
+    // Discard the memtable and recover through the actual checkpoint/WAL 
recovery path.
+    final WALRecoverManager recoverManager = WALRecoverManager.getInstance();
+    recoverManager.clear();
+    final UnsealedTsFileRecoverPerformer performer =
+        new UnsealedTsFileRecoverPerformer(
+            new TsFileResource(lateFile), true, recovered -> 
assertFalse(recovered.canWrite()));
+    recoverManager.addRecoverPerformer(performer);
+    recoverManager.setAllDataRegionScannedLatch(new 
ExceptionalCountDownLatch(0));
+    recoverManager.recover();
+    assertEquals(WALRecoverListener.Status.SUCCESS, 
performer.getRecoverListener().waitForResult());
+    assertSingleRow(flushedFile, 1);
+    assertSingleRow(lateFile, 2);
+  }
+
+  private void writeAndAssertSuccess(long time) throws Exception {
+    assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+        stateMachine.write((IConsensusRequest) insertRow(time)).getCode());
+  }
+
+  private void writeUncheckedAndAssertSuccess(long time) throws Exception {
+    assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+        insertRow(time).accept(new DataExecutionVisitor(), 
dataRegion).getCode());
+  }
+
+  private static InsertRowNode insertRow(long time) throws Exception {
+    final InsertRowNode node =
+        new InsertRowNode(
+            new PlanNodeId("shutdown"),
+            new PartialPath(DEVICE),
+            false,
+            new String[] {MEASUREMENT},
+            new TSDataType[] {TSDataType.INT32},
+            time,
+            new Object[] {(int) time * 10},
+            false);
+    node.setMeasurementSchemas(
+        new MeasurementSchema[] {new MeasurementSchema(MEASUREMENT, 
TSDataType.INT32)});
+    return node;
+  }
+
+  private static void assertSingleRow(File file, long time) throws Exception {
+    assertRows(file, time);
+  }
+
+  private static void assertRows(File file, long... times) throws Exception {
+    try (TsFileSequenceReader sequenceReader = new 
TsFileSequenceReader(file.getPath());
+        TsFileReader reader = new TsFileReader(sequenceReader)) {
+      final QueryDataSet dataSet =
+          reader.query(
+              QueryExpression.create(
+                  Collections.singletonList(new Path(DEVICE, MEASUREMENT, 
false)), null));
+      for (long time : times) {
+        assertTrue(dataSet.hasNext());
+        final RowRecord row = dataSet.next();
+        assertEquals(time, row.getTimestamp());
+        assertEquals((int) time * 10, row.getFields().get(0).getIntV());
+      }
+      assertFalse(dataSet.hasNext());
+    }
+  }
+}

Reply via email to