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

CRZbulabula 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 f60066e9609 Trigger node status recovery snapshots after cluster 
registration (#18965)
f60066e9609 is described below

commit f60066e9609cfdfda65a6916c12c20dca7f844d1
Author: d-wang-commit <[email protected]>
AuthorDate: Thu Oct 22 13:01:06 2026 +0800

    Trigger node status recovery snapshots after cluster registration (#18965)
---
 .../it/cluster/IoTDBNodeStatusPersistenceIT.java   | 164 +++++++++++++++------
 1 file changed, 118 insertions(+), 46 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/cluster/IoTDBNodeStatusPersistenceIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/cluster/IoTDBNodeStatusPersistenceIT.java
index 6829780c2f3..4030d7bd615 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/cluster/IoTDBNodeStatusPersistenceIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/cluster/IoTDBNodeStatusPersistenceIT.java
@@ -22,23 +22,36 @@ package org.apache.iotdb.confignode.it.cluster;
 import org.apache.iotdb.common.rpc.thrift.TConfigNodeLocation;
 import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.client.ClientPoolFactory;
 import org.apache.iotdb.commons.client.IClientManager;
 import org.apache.iotdb.commons.client.exception.ClientManagerException;
 import org.apache.iotdb.commons.client.sync.SyncConfigNodeIServiceClient;
 import org.apache.iotdb.commons.client.sync.SyncDataNodeInternalServiceClient;
 import org.apache.iotdb.commons.cluster.NodeStatus;
+import org.apache.iotdb.commons.consensus.ConfigRegionId;
+import org.apache.iotdb.confignode.conf.ConfigNodeDescriptor;
 import org.apache.iotdb.confignode.persistence.node.NodeInfo;
 import org.apache.iotdb.confignode.rpc.thrift.TDataNodeInfo;
 import org.apache.iotdb.confignode.rpc.thrift.TShowClusterResp;
 import org.apache.iotdb.confignode.rpc.thrift.TShowDataNodesResp;
 import org.apache.iotdb.consensus.ConsensusFactory;
+import org.apache.iotdb.consensus.ratis.utils.Utils;
 import org.apache.iotdb.it.env.EnvFactory;
 import org.apache.iotdb.it.env.cluster.node.ConfigNodeWrapper;
 import org.apache.iotdb.it.framework.IoTDBTestRunner;
 import org.apache.iotdb.itbase.category.ClusterIT;
 import org.apache.iotdb.rpc.TSStatusCode;
 
+import org.apache.ratis.client.RaftClient;
+import org.apache.ratis.client.RaftClientConfigKeys;
+import org.apache.ratis.conf.RaftProperties;
+import org.apache.ratis.protocol.RaftClientReply;
+import org.apache.ratis.protocol.RaftGroup;
+import org.apache.ratis.protocol.RaftPeer;
+import org.apache.ratis.protocol.RaftPeerId;
+import org.apache.ratis.retry.RetryPolicies;
+import org.apache.ratis.util.TimeDuration;
 import org.apache.thrift.TException;
 import org.apache.tsfile.utils.Pair;
 import org.junit.After;
@@ -63,7 +76,6 @@ import java.util.stream.Stream;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.fail;
 
 @RunWith(IoTDBTestRunner.class)
 @Category({ClusterIT.class})
@@ -112,7 +124,7 @@ public class IoTDBNodeStatusPersistenceIT {
 
   @Test
   public void testStoppedNodesSurviveSnapshotRecovery() throws Exception {
-    initCluster(3, 1);
+    initCluster(3, Integer.MAX_VALUE);
     final int stoppedConfigNodeIndex = 
(EnvFactory.getEnv().getLeaderConfigNodeIndex() + 1) % 3;
     stopAndReportNodes(stoppedConfigNodeIndex);
     assertNodeStatuses(stoppedConfigNodeIndex, NodeStatus.Stopped);
@@ -129,10 +141,11 @@ public class IoTDBNodeStatusPersistenceIT {
     final int stoppedDataNodeId = 
cluster.getDataNodeList().get(0).getDataNodeId();
 
     // Verify actual completed snapshots, including their contents, before 
restarting the quorum.
-    // A low snapshot threshold alone would not prove snapshot recovery was 
exercised.
+    // Trigger snapshots only after registration and status persistence have 
completed.
     for (ConfigNodeWrapper configNode : liveConfigNodes) {
       awaitNodeStatusSnapshot(
           configNode,
+          cluster,
           Map.of(
               stoppedConfigNodeId, new Pair<>(NodeStatus.Stopped, null),
               stoppedDataNodeId, new Pair<>(NodeStatus.Stopped, null)));
@@ -186,19 +199,22 @@ public class IoTDBNodeStatusPersistenceIT {
 
   @Test
   public void testReadOnlyReasonSurvivesSnapshotRecovery() throws Exception {
-    initCluster(3, 1);
+    initCluster(3, Integer.MAX_VALUE);
     setDataNodeSystemStatus(NodeStatus.ReadOnly);
     awaitDataNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL);
 
-    final int dataNodeId;
+    final TShowClusterResp cluster;
     try (SyncConfigNodeIServiceClient client =
         (SyncConfigNodeIServiceClient) 
EnvFactory.getEnv().getLeaderConfigNodeConnection()) {
-      dataNodeId = 
client.showCluster().getDataNodeList().get(0).getDataNodeId();
+      cluster = client.showCluster();
+      assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
cluster.getStatus().getCode());
     }
+    final int dataNodeId = cluster.getDataNodeList().get(0).getDataNodeId();
     // Inspect completed snapshots so live heartbeats cannot hide a missing 
durable reason.
     for (ConfigNodeWrapper configNode : getLiveConfigNodes()) {
       awaitNodeStatusSnapshot(
           configNode,
+          cluster,
           Collections.singletonMap(dataNodeId, new Pair<>(NodeStatus.ReadOnly, 
NodeStatus.MANUAL)));
     }
     restartLiveConfigNodes();
@@ -287,12 +303,16 @@ public class IoTDBNodeStatusPersistenceIT {
     // shutdown hooks on every platform. These tests exercise the reports' 
durable handling.
     try (SyncConfigNodeIServiceClient client =
         (SyncConfigNodeIServiceClient) 
EnvFactory.getEnv().getLeaderConfigNodeConnection()) {
+      final TSStatus configNodeStatus = 
client.reportConfigNodeShutdown(configNodeLocation);
       assertEquals(
+          configNodeStatus.toString(),
           TSStatusCode.SUCCESS_STATUS.getStatusCode(),
-          client.reportConfigNodeShutdown(configNodeLocation).getCode());
+          configNodeStatus.getCode());
+      final TSStatus dataNodeStatus = 
client.reportDataNodeShutdown(dataNodeLocation);
       assertEquals(
+          dataNodeStatus.toString(),
           TSStatusCode.SUCCESS_STATUS.getStatusCode(),
-          client.reportDataNodeShutdown(dataNodeLocation).getCode());
+          dataNodeStatus.getCode());
     }
   }
 
@@ -330,50 +350,102 @@ public class IoTDBNodeStatusPersistenceIT {
   }
 
   private void awaitNodeStatusSnapshot(
-      ConfigNodeWrapper configNode, Map<Integer, Pair<NodeStatus, String>> 
expectedStatuses)
+      ConfigNodeWrapper configNode,
+      TShowClusterResp cluster,
+      Map<Integer, Pair<NodeStatus, String>> expectedStatuses)
       throws Exception {
-    for (int retry = 0; retry < 60; retry++) {
-      final List<Path> snapshotFiles = new ArrayList<>();
-      try {
-        final List<Path> groups;
-        try (Stream<Path> paths =
-            Files.list(Paths.get(configNode.getNodePath(), "data", 
"confignode", "consensus"))) {
-          groups = 
paths.filter(Files::isDirectory).collect(Collectors.toList());
-        }
-        // Never walk temporary snapshot directories: an open directory handle 
can prevent
-        // Ratis from atomically renaming them on Windows.
-        for (Path group : groups) {
-          try (Stream<Path> paths = Files.list(group.resolve("sm"))) {
-            snapshotFiles.addAll(
-                paths
-                    .filter(path -> 
path.getFileName().toString().matches("\\d+_\\d+"))
-                    .map(path -> path.resolve("node_info.bin"))
-                    .collect(Collectors.toList()));
-          }
-        }
-      } catch (IOException | UncheckedIOException e) {
-        // Ratis can delete an older snapshot while this directory tree is 
being enumerated.
-        TimeUnit.SECONDS.sleep(1);
-        continue;
-      }
-      for (Path snapshotFile : snapshotFiles) {
-        final NodeInfo recovered = new NodeInfo();
+    final List<RaftPeer> peers =
+        cluster.getConfigNodeList().stream()
+            .map(
+                node ->
+                    Utils.fromNodeInfoAndPriorityToRaftPeer(
+                        node.getConfigNodeId(), node.getConsensusEndPoint(), 
0))
+            .collect(Collectors.toList());
+    final RaftPeerId peerId =
+        Utils.fromNodeIdToRaftPeerId(
+            cluster.getConfigNodeList().stream()
+                .filter(node -> node.getInternalEndPoint().getPort() == 
configNode.getPort())
+                .findFirst()
+                .orElseThrow(AssertionError::new)
+                .getConfigNodeId());
+    final RaftProperties properties = new RaftProperties();
+    RaftClientConfigKeys.Rpc.setRequestTimeout(
+        properties, TimeDuration.valueOf(5, TimeUnit.SECONDS));
+    final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60);
+    Throwable lastFailure = null;
+    try (RaftClient client =
+        RaftClient.newBuilder()
+            .setRaftGroup(
+                RaftGroup.valueOf(
+                    Utils.fromConsensusGroupIdToRaftGroupId(
+                        new ConfigRegionId(
+                            
ConfigNodeDescriptor.getInstance().getConf().getConfigRegionId())),
+                    peers))
+            .setProperties(properties)
+            .setRetryPolicy(RetryPolicies.noRetry())
+            .build()) {
+      for (int retry = 0; retry < 60 && System.nanoTime() < deadline; retry++) 
{
+        final List<Path> snapshotFiles = new ArrayList<>();
         try {
-          recovered.processLoadSnapshot(snapshotFile.getParent().toFile());
-        } catch (IOException e) {
-          // A newer snapshot may have replaced this one since the directory 
was enumerated.
+          // A follower can still be applying the status plan after the leader 
acknowledges it.
+          // Request another completed snapshot until its persisted contents 
match the expectation.
+          final RaftClientReply reply =
+              client
+                  .getSnapshotManagementApi(peerId)
+                  .create(
+                      true,
+                      Math.max(
+                          1,
+                          Math.min(
+                              5000, TimeUnit.NANOSECONDS.toMillis(deadline - 
System.nanoTime()))));
+          if (!reply.isSuccess()) {
+            throw new IOException(
+                "Snapshot request failed on " + configNode.getId() + ": " + 
reply);
+          }
+          final List<Path> groups;
+          try (Stream<Path> paths =
+              Files.list(Paths.get(configNode.getNodePath(), "data", 
"confignode", "consensus"))) {
+            groups = 
paths.filter(Files::isDirectory).collect(Collectors.toList());
+          }
+          // Never walk temporary snapshot directories: an open directory 
handle can prevent
+          // Ratis from atomically renaming them on Windows.
+          for (Path group : groups) {
+            try (Stream<Path> paths = Files.list(group.resolve("sm"))) {
+              snapshotFiles.addAll(
+                  paths
+                      .filter(path -> 
path.getFileName().toString().matches("\\d+_\\d+"))
+                      .map(path -> path.resolve("node_info.bin"))
+                      .collect(Collectors.toList()));
+            }
+          }
+        } catch (IOException | UncheckedIOException e) {
+          lastFailure = e;
+          // Ratis can delete an older snapshot while this directory tree is 
being enumerated.
+          TimeUnit.SECONDS.sleep(1);
           continue;
         }
-        if (expectedStatuses.entrySet().stream()
-            .allMatch(
-                entry ->
-                    java.util.Objects.equals(
-                        entry.getValue(), 
recovered.getPersistedNodeStatus(entry.getKey())))) {
-          return;
+        for (Path snapshotFile : snapshotFiles) {
+          final NodeInfo recovered = new NodeInfo();
+          try {
+            recovered.processLoadSnapshot(snapshotFile.getParent().toFile());
+          } catch (IOException e) {
+            lastFailure = e;
+            // A newer snapshot may have replaced this one since the directory 
was enumerated.
+            continue;
+          }
+          if (expectedStatuses.entrySet().stream()
+              .allMatch(
+                  entry ->
+                      java.util.Objects.equals(
+                          entry.getValue(), 
recovered.getPersistedNodeStatus(entry.getKey())))) {
+            return;
+          }
         }
+        TimeUnit.SECONDS.sleep(1);
       }
-      TimeUnit.SECONDS.sleep(1);
     }
-    fail("No completed snapshot contains " + expectedStatuses + " on " + 
configNode.getId());
+    throw new AssertionError(
+        "No completed snapshot contains " + expectedStatuses + " on " + 
configNode.getId(),
+        lastFailure);
   }
 }

Reply via email to