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 4405647b0f2 Fix schema partition lookup during ConfigNode removal 
(#18686)
4405647b0f2 is described below

commit 4405647b0f20b7bf78641073a6a459c86ad529e0
Author: d-wang-commit <[email protected]>
AuthorDate: Mon Sep 21 15:04:44 2026 +0800

    Fix schema partition lookup during ConfigNode removal (#18686)
---
 .../IoTDBRemoveConfigNodeSchemaPartitionIT.java    | 262 +++++++++++++++++++++
 .../iotdb/confignode/manager/ConfigManager.java    |   5 +
 2 files changed, 267 insertions(+)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/removeconfignode/IoTDBRemoveConfigNodeSchemaPartitionIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/removeconfignode/IoTDBRemoveConfigNodeSchemaPartitionIT.java
new file mode 100644
index 00000000000..8e2c6b957cb
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/removeconfignode/IoTDBRemoveConfigNodeSchemaPartitionIT.java
@@ -0,0 +1,262 @@
+/*
+ * 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.confignode.it.removeconfignode;
+
+import org.apache.iotdb.common.rpc.thrift.TConfigNodeLocation;
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSeriesPartitionSlot;
+import org.apache.iotdb.commons.client.property.ThriftClientProperty;
+import org.apache.iotdb.commons.client.sync.SyncConfigNodeIServiceClient;
+import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionTableResp;
+import org.apache.iotdb.consensus.ConsensusFactory;
+import org.apache.iotdb.db.protocol.client.ConfigNodeClient;
+import org.apache.iotdb.isession.ITableSession;
+import org.apache.iotdb.isession.SessionDataSet;
+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.TableClusterIT;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.thrift.transport.TTransport;
+import org.apache.tsfile.enums.ColumnCategory;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.write.record.Tablet;
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(IoTDBTestRunner.class)
+@Category(TableClusterIT.class)
+public class IoTDBRemoveConfigNodeSchemaPartitionIT {
+
+  private static final String DATABASE = "cn_removal";
+
+  @Before
+  public void setUp() throws Exception {
+    EnvFactory.getEnv()
+        .getConfig()
+        .getCommonConfig()
+        .setConfigNodeConsensusProtocolClass(ConsensusFactory.RATIS_CONSENSUS)
+        // Seed the only slot before removal, so this test exercises reads of 
existing partitions.
+        .setSeriesSlotNum(1)
+        .setSchemaReplicationFactor(1)
+        .setDataReplicationFactor(1);
+    EnvFactory.getEnv().initClusterEnvironment(3, 1);
+    try (ITableSession session = 
EnvFactory.getEnv().getTableSessionConnection()) {
+      session.executeNonQueryStatement("CREATE DATABASE " + DATABASE);
+      session.executeNonQueryStatement("USE " + DATABASE);
+      session.executeNonQueryStatement(
+          "CREATE TABLE devices (device STRING TAG, value INT64 FIELD)");
+      insertDevice(session, 0);
+    }
+  }
+
+  @After
+  public void tearDown() {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  @Test
+  public void testSchemaPartitionOnDeletedConfigRegion() throws Exception {
+    final int leaderIndex = EnvFactory.getEnv().getLeaderConfigNodeIndex();
+    final int removedIndex = (leaderIndex + 1) % 3;
+    final ConfigNodeWrapper removedNode = 
EnvFactory.getEnv().getConfigNodeWrapper(removedIndex);
+    final TEndPoint removedEndpoint = new TEndPoint(removedNode.getIp(), 
removedNode.getPort());
+    final Map<String, List<TSeriesPartitionSlot>> request =
+        Collections.singletonMap(DATABASE, Collections.emptyList());
+
+    try (SyncConfigNodeIServiceClient leader =
+            (SyncConfigNodeIServiceClient) 
EnvFactory.getEnv().getLeaderConfigNodeConnection();
+        SyncConfigNodeIServiceClient oldConnection =
+            (SyncConfigNodeIServiceClient)
+                EnvFactory.getEnv().getConfigNodeConnection(removedIndex)) {
+      final List<TConfigNodeLocation> locations = 
leader.showCluster().getConfigNodeList();
+      final TConfigNodeLocation removedLocation =
+          locations.stream()
+              .filter(location -> 
location.getInternalEndPoint().equals(removedEndpoint))
+              .findFirst()
+              .get();
+      final TSchemaPartitionTableResp expected = 
leader.getSchemaPartitionTableWithSlots(request);
+      assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
expected.getStatus().getCode());
+      assertFalse(expected.getSchemaPartitionTable().get(DATABASE).isEmpty());
+
+      final List<TEndPoint> staleEndpoints = new ArrayList<>();
+      staleEndpoints.add(removedEndpoint);
+      locations.stream()
+          .map(TConfigNodeLocation::getInternalEndPoint)
+          .filter(endpoint -> !endpoint.equals(removedEndpoint))
+          .forEach(staleEndpoints::add);
+      // Use the actual DN retry client against real CN processes. This client 
is not pooled.
+      try (ConfigNodeClient retryClient =
+          new ConfigNodeClient(
+              staleEndpoints,
+              new 
ThriftClientProperty.Builder().setConnectionTimeoutMs(30_000).build(),
+              null) {
+            @Override
+            public void close() {
+              invalidate();
+            }
+          }) {
+        assertEquals(removedEndpoint, retryClient.getConfigNode());
+        final TTransport oldTransport = retryClient.getTransport();
+        oldConnection.setTimeout(30_000);
+
+        // Execute the same deletion RPC as RemoveConfigNodeProcedure, but 
keep the process alive.
+        // This deterministically holds the delete-peer / stop-node window 
without racing its 5s
+        // timer.
+        assertEquals(
+            TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+            oldConnection.deleteConfigNodePeer(removedLocation).getCode());
+        final TSchemaPartitionTableResp rejected =
+            oldConnection.getSchemaPartitionTableWithSlots(request);
+        assertEquals(
+            rejected.getStatus().toString(),
+            TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode(),
+            rejected.getStatus().getCode());
+        assertFalse(rejected.getStatus().isSetRedirectNode());
+        assertFalse(rejected.isSetSchemaPartitionTable());
+
+        // The old TCP connection must receive a retryable response, then move 
to a live CN.
+        assertTrue(oldTransport.isOpen());
+        final TSchemaPartitionTableResp recovered =
+            retryClient.getSchemaPartitionTableWithSlots(request);
+        assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
recovered.getStatus().getCode());
+        assertEquals(expected.getSchemaPartitionTable(), 
recovered.getSchemaPartitionTable());
+        assertNotSame(oldTransport, retryClient.getTransport());
+        assertFalse(removedEndpoint.equals(retryClient.getConfigNode()));
+      }
+    }
+  }
+
+  @Test
+  public void testTableReadWriteOnExistingPartitionsDuringLeaderRemoval() 
throws Exception {
+    final int leaderIndex = EnvFactory.getEnv().getLeaderConfigNodeIndex();
+    final ConfigNodeWrapper removedNode = 
EnvFactory.getEnv().getConfigNodeWrapper(leaderIndex);
+    final int removedId;
+    try (SyncConfigNodeIServiceClient leader =
+        (SyncConfigNodeIServiceClient) 
EnvFactory.getEnv().getLeaderConfigNodeConnection()) {
+      removedId =
+          leader.showCluster().getConfigNodeList().stream()
+              .filter(location -> location.getInternalEndPoint().getPort() == 
removedNode.getPort())
+              .findFirst()
+              .get()
+              .getConfigNodeId();
+    }
+
+    final AtomicBoolean stopped = new AtomicBoolean();
+    final CountDownLatch started = new CountDownLatch(1);
+    final ExecutorService executor = Executors.newSingleThreadExecutor();
+    final Future<Integer> workload =
+        executor.submit(
+            () -> {
+              try (ITableSession session =
+                  
EnvFactory.getEnv().getTableSessionConnectionWithDB(DATABASE)) {
+                int inserted = 0;
+                while (!stopped.get()) {
+                  // New devices exercise metadata fetching; an unrestricted 
query traverses
+                  // devices.
+                  insertDevice(session, ++inserted);
+                  assertRowCount(session, inserted + 1);
+                  started.countDown();
+                  TimeUnit.MILLISECONDS.sleep(50);
+                }
+                return inserted;
+              }
+            });
+    try {
+      assertTrue("The table workload did not start", started.await(30, 
TimeUnit.SECONDS));
+      try (ITableSession admin = 
EnvFactory.getEnv().getTableSessionConnection()) {
+        admin.executeNonQueryStatement("REMOVE CONFIGNODE " + removedId);
+      }
+      Awaitility.await()
+          .atMost(90, TimeUnit.SECONDS)
+          .untilAsserted(
+              () -> {
+                try (SyncConfigNodeIServiceClient leader =
+                    (SyncConfigNodeIServiceClient)
+                        EnvFactory.getEnv().getLeaderConfigNodeConnection()) {
+                  final List<TConfigNodeLocation> remaining =
+                      leader.showCluster().getConfigNodeList();
+                  assertEquals(2, remaining.size());
+                  assertTrue(
+                      remaining.stream().noneMatch(node -> 
node.getConfigNodeId() == removedId));
+                }
+                assertFalse(removedNode.getInstance().isAlive());
+              });
+      stopped.set(true);
+      // Propagate every SELECT/insertTablet failure; do not hide failures 
with application retries.
+      final int inserted = workload.get(60, TimeUnit.SECONDS);
+      assertTrue(inserted > 0);
+      try (ITableSession session = 
EnvFactory.getEnv().getTableSessionConnectionWithDB(DATABASE)) {
+        insertDevice(session, inserted + 1);
+        assertRowCount(session, inserted + 2);
+      }
+    } finally {
+      stopped.set(true);
+      workload.cancel(true);
+      executor.shutdownNow();
+      assertTrue(executor.awaitTermination(60, TimeUnit.SECONDS));
+    }
+  }
+
+  private static void insertDevice(ITableSession session, int index) throws 
Exception {
+    final Tablet tablet =
+        new Tablet(
+            "devices",
+            Arrays.asList("device", "value"),
+            Arrays.asList(TSDataType.STRING, TSDataType.INT64),
+            Arrays.asList(ColumnCategory.TAG, ColumnCategory.FIELD),
+            1);
+    tablet.addTimestamp(0, index);
+    tablet.addValue("device", 0, "d" + index);
+    tablet.addValue("value", 0, (long) index);
+    session.insert(tablet);
+  }
+
+  private static void assertRowCount(ITableSession session, int expected) 
throws Exception {
+    try (SessionDataSet result =
+        session.executeQueryStatement("SELECT count(value) FROM devices")) {
+      assertTrue(result.hasNext());
+      assertEquals(expected, result.next().getFields().get(0).getLongV());
+      assertFalse(result.hasNext());
+    }
+  }
+}
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java
index f86681f2bbe..5041b77136b 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java
@@ -909,6 +909,11 @@ public class ConfigManager implements IManager {
   @Override
   public TSchemaPartitionTableResp getSchemaPartition(
       final Map<String, List<TSeriesPartitionSlot>> dbSlotMap) {
+    final TSStatus status = confirmLeader();
+    if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+      return new TSchemaPartitionTableResp().setStatus(status);
+    }
+
     // Construct empty response
     TSchemaPartitionTableResp resp = new TSchemaPartitionTableResp();
     // Return empty resp if the partitionSlotsMap is empty

Reply via email to