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 20cab121be9 [Fix] Prevent stale schema queue closes after leader 
transfer (#18948)
20cab121be9 is described below

commit 20cab121be9de8a901ec4d34d31123641c8f16da
Author: Caideyipi <[email protected]>
AuthorDate: Wed Oct 21 15:44:28 2026 +0800

    [Fix] Prevent stale schema queue closes after leader transfer (#18948)
    
    * [Fix] Prevent stale schema queue closes from following leader transfers
    
    * [Pipe] Wait for historical transfer before asserting mods
---
 .../treemodel/auto/basic/IoTDBPipeDataSinkIT.java  | 39 ++++++++++++++++++++
 .../iotdb/consensus/ratis/RatisConsensus.java      |  6 +++
 .../iotdb/consensus/ratis/RatisConsensusTest.java  | 40 ++++++++++++++++++++
 .../plan/node/pipe/PipeOperateSchemaQueueNode.java |  7 ++++
 .../node/pipe/PipeOperateSchemaQueueNodeTest.java  | 43 ++++++++++++++++++++++
 .../iotdb/commons/request/IConsensusRequest.java   |  8 ++++
 6 files changed, 143 insertions(+)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/basic/IoTDBPipeDataSinkIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/basic/IoTDBPipeDataSinkIT.java
index 03835e7743b..30cee019f66 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/basic/IoTDBPipeDataSinkIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/basic/IoTDBPipeDataSinkIT.java
@@ -41,15 +41,18 @@ import org.junit.runner.RunWith;
 
 import java.nio.charset.Charset;
 import java.sql.Connection;
+import java.sql.ResultSet;
 import java.sql.Statement;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 
 import static org.apache.iotdb.db.it.IoTDBRestServiceIT.getHttpPost;
+import static org.awaitility.Awaitility.await;
 
 @RunWith(IoTDBTestRunner.class)
 @Category({MultiClusterIT2DualTreeAutoBasic.class})
@@ -582,6 +585,7 @@ public class IoTDBPipeDataSinkIT extends 
AbstractPipeDualTreeModelAutoIT {
                 "create pipe test with source 
('source.realtime.mode'='stream','inclusion'='data','path'='root.sg_nonAligned.非对齐序列带有encoding和压缩方式.**','source.realtime.enable'='true','mods.enable'='true')
 with sink ('sink'='iotdb-thrift-sink', 'sink.node-urls'='%s')",
                 
receiverEnv.getDataNodeWrapperList().get(0).getIpAndPortString())));
 
+    awaitHistoricalPipeCompletion("test");
     TestUtils.assertDataEventuallyOnEnv(
         receiverEnv,
         "count timeseries root.sg_nonAligned.非对齐序列带有encoding和压缩方式.*",
@@ -602,6 +606,7 @@ public class IoTDBPipeDataSinkIT extends 
AbstractPipeDualTreeModelAutoIT {
                 "create pipe test with source 
('source.realtime.mode'='stream','inclusion'='data','path'='root.sg_nonAligned.非对齐序列带有encoding和压缩方式.**','source.realtime.enable'='true','mods.enable'='true')
 with sink ('sink'='iotdb-thrift-sink', 'sink.node-urls'='%s')",
                 
receiverEnv.getDataNodeWrapperList().get(0).getIpAndPortString())));
 
+    awaitHistoricalPipeCompletion("test");
     TestUtils.assertDataEventuallyOnEnv(
         receiverEnv,
         "count timeseries root.sg_nonAligned.非对齐序列带有encoding和压缩方式.*",
@@ -614,4 +619,38 @@ public class IoTDBPipeDataSinkIT extends 
AbstractPipeDualTreeModelAutoIT {
         "count(timeseries),",
         Collections.singleton("4,"));
   }
+
+  private void awaitHistoricalPipeCompletion(final String pipeName) {
+    await()
+        .pollInSameThread()
+        .pollDelay(1L, TimeUnit.SECONDS)
+        .pollInterval(1L, TimeUnit.SECONDS)
+        .atMost(600L, TimeUnit.SECONDS)
+        .untilAsserted(
+            () -> {
+              boolean sourcePipeFound = false;
+              boolean sourcePipeCompleted = true;
+              boolean realtimePipeFound = false;
+
+              for (final DataNodeWrapper dataNode : 
senderEnv.getDataNodeWrapperList()) {
+                try (final Connection connection = 
senderEnv.getConnection(dataNode);
+                    final Statement statement = connection.createStatement();
+                    final ResultSet resultSet = statement.executeQuery("show 
pipes")) {
+                  while (resultSet.next()) {
+                    final String id = resultSet.getString(1);
+                    if (id.equals(pipeName) || id.equals(pipeName + 
"_history")) {
+                      sourcePipeFound = true;
+                      sourcePipeCompleted &= 
"0".equals(resultSet.getString(8));
+                    } else if (id.equals(pipeName + "_realtime")) {
+                      realtimePipeFound = true;
+                    }
+                  }
+                }
+              }
+
+              // The historical sub-pipe may be auto-dropped after all events 
are committed.
+              Assert.assertTrue(sourcePipeFound || realtimePipeFound);
+              Assert.assertTrue(!sourcePipeFound || sourcePipeCompleted);
+            });
+  }
 }
diff --git 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/ratis/RatisConsensus.java
 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/ratis/RatisConsensus.java
index b7369546e7c..c2028cfd0da 100644
--- 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/ratis/RatisConsensus.java
+++ 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/ratis/RatisConsensus.java
@@ -373,6 +373,12 @@ class RatisConsensus implements IConsensus {
       }
     }
 
+    // A leader-local decision may already be stale after a leadership change. 
Let the current
+    // leader make its own decision instead of replaying the old leader's 
request there.
+    if (!request.isForwardable()) {
+      return StatusUtils.getStatus(TSStatusCode.REDIRECTION_RECOMMEND);
+    }
+
     // 2. try raft client
     TSStatus writeResult;
     try (AutoCloseable ignored =
diff --git 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/ratis/RatisConsensusTest.java
 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/ratis/RatisConsensusTest.java
index 842ea78af72..3af937ee4e4 100644
--- 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/ratis/RatisConsensusTest.java
+++ 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/ratis/RatisConsensusTest.java
@@ -20,6 +20,7 @@
 package org.apache.iotdb.consensus.ratis;
 
 import org.apache.iotdb.commons.consensus.ConsensusGroupId;
+import org.apache.iotdb.commons.request.IConsensusRequest;
 import org.apache.iotdb.consensus.IStateMachine;
 import org.apache.iotdb.consensus.common.ConsensusGroup;
 import org.apache.iotdb.consensus.common.Peer;
@@ -29,6 +30,7 @@ import 
org.apache.iotdb.consensus.exception.ConsensusGroupAlreadyExistException;
 import org.apache.iotdb.consensus.exception.ConsensusGroupNotExistException;
 import 
org.apache.iotdb.consensus.exception.PeerAlreadyInConsensusGroupException;
 import org.apache.iotdb.consensus.exception.PeerNotInConsensusGroupException;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.ratis.util.TimeDuration;
 import org.junit.After;
@@ -113,6 +115,44 @@ public class RatisConsensusTest {
     doConsensus(0, 10, 10);
   }
 
+  @Test
+  public void testNonForwardableWriteAfterLeaderTransfer() throws Exception {
+    for (RatisConsensus server : servers) {
+      server.createLocalPeer(gid, peers);
+    }
+    miniCluster.waitUntilActiveLeaderElectedAndReady();
+
+    final int oldLeaderId = servers.get(0).getLeader(gid).getNodeId();
+    final int newLeaderId = (oldLeaderId + 1) % servers.size();
+    final RatisConsensus oldLeader = servers.get(oldLeaderId);
+    final IConsensusRequest leaderLocalRequest =
+        new 
TestUtils.TestRequest(TestUtils.TestRequest.incrRequest().serializeToByteBuffer())
 {
+          @Override
+          public boolean isForwardable() {
+            return false;
+          }
+        };
+
+    oldLeader.transferLeader(gid, peers.get(newLeaderId));
+    miniCluster.waitUntilActiveLeaderElectedAndReady();
+    Assert.assertEquals(newLeaderId, oldLeader.getLeader(gid).getNodeId());
+
+    // The new leader has already made its own decision before the old request 
arrives.
+    Assert.assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+        servers.get(newLeaderId).write(gid, leaderLocalRequest).getCode());
+    Assert.assertEquals(
+        TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode(),
+        oldLeader.write(gid, leaderLocalRequest).getCode());
+    Assert.assertEquals(1, miniCluster.mustRead(newLeaderId));
+
+    // Ordinary writes issued to a follower must still reach the current 
leader.
+    Assert.assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+        oldLeader.write(gid, TestUtils.TestRequest.incrRequest()).getCode());
+    Assert.assertEquals(2, miniCluster.mustRead(newLeaderId));
+  }
+
   @Test
   public void addMemberToGroup() throws Exception {
     List<Peer> original = peers.subList(0, 1);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/pipe/PipeOperateSchemaQueueNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/pipe/PipeOperateSchemaQueueNode.java
index 14512835068..3a24635ee5d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/pipe/PipeOperateSchemaQueueNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/pipe/PipeOperateSchemaQueueNode.java
@@ -54,6 +54,13 @@ public class PipeOperateSchemaQueueNode extends PlanNode {
     return isOpen;
   }
 
+  @Override
+  public boolean isForwardable() {
+    // A close is decided from the local pipe metadata. After a leader change 
the new leader may
+    // already have newer metadata and an active source, so an old close must 
not be forwarded.
+    return isOpen;
+  }
+
   @Override
   public List<PlanNode> getChildren() {
     return Collections.emptyList();
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/pipe/PipeOperateSchemaQueueNodeTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/pipe/PipeOperateSchemaQueueNodeTest.java
new file mode 100644
index 00000000000..491c46fabdd
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/pipe/PipeOperateSchemaQueueNodeTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.queryengine.plan.planner.plan.node.pipe;
+
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import 
org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeType;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PipeOperateSchemaQueueNodeTest {
+
+  @Test
+  public void testCloseCannotBeForwardedToNewLeader() {
+    final PipeOperateSchemaQueueNode close =
+        new PipeOperateSchemaQueueNode(new PlanNodeId("close"), false);
+    Assert.assertFalse(close.isForwardable());
+    Assert.assertFalse(close.clone().isForwardable());
+    
Assert.assertFalse(PlanNodeType.deserialize(close.serializeToByteBuffer()).isForwardable());
+
+    final PipeOperateSchemaQueueNode open =
+        new PipeOperateSchemaQueueNode(new PlanNodeId("open"), true);
+    Assert.assertTrue(open.isForwardable());
+    
Assert.assertTrue(PlanNodeType.deserialize(open.serializeToByteBuffer()).isForwardable());
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/request/IConsensusRequest.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/request/IConsensusRequest.java
index 204b5dd75a9..490c88f2b3f 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/request/IConsensusRequest.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/request/IConsensusRequest.java
@@ -41,6 +41,14 @@ public interface IConsensusRequest {
     return 0;
   }
 
+  /**
+   * Whether a consensus implementation may forward this write to another 
leader. Requests based on
+   * the local leader's runtime state must not be forwarded after that leader 
steps down.
+   */
+  default boolean isForwardable() {
+    return true;
+  }
+
   /**
    * Returns whether this request materializes its bytes only when it is about 
to be sent, instead
    * of when it is handed over to the replication layer.

Reply via email to