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 02ae00db9bc Wait for region leader election before region migration
(#18699)
02ae00db9bc is described below
commit 02ae00db9bcf8d99fdf085e3162559d2f47a1cc3
Author: Yongzao <[email protected]>
AuthorDate: Wed Sep 23 11:08:52 2026 +0800
Wait for region leader election before region migration (#18699)
* Fix region migration NPE after ConfigNode leader failover
* Wait for region leader election before migration
---
.../procedure/env/RegionMaintainHandler.java | 22 +-
.../procedure/env/RegionMaintainHandlerTest.java | 235 +++++++++++++++++++++
2 files changed, 255 insertions(+), 2 deletions(-)
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/env/RegionMaintainHandler.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/env/RegionMaintainHandler.java
index 4def6fc78b3..22c65e1798b 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/env/RegionMaintainHandler.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/env/RegionMaintainHandler.java
@@ -51,6 +51,7 @@ import
org.apache.iotdb.confignode.consensus.request.write.partition.RemoveRegio
import
org.apache.iotdb.confignode.consensus.request.write.region.CreateRegionGroupsPlan;
import org.apache.iotdb.confignode.i18n.ProcedureMessages;
import org.apache.iotdb.confignode.manager.ConfigManager;
+import org.apache.iotdb.confignode.manager.load.LoadManager;
import
org.apache.iotdb.confignode.manager.load.cache.consensus.ConsensusGroupHeartbeatSample;
import org.apache.iotdb.confignode.procedure.exception.ProcedureException;
import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq;
@@ -843,7 +844,7 @@ public class RegionMaintainHandler {
(CONF.getSchemaRegionRatisRpcLeaderElectionTimeoutMaxMs()
+ CONF.getSchemaRegionRatisRpcLeaderElectionTimeoutMinMs())
/ 2;
- Integer leaderId =
configManager.getLoadManager().getRegionLeaderMap().get(regionId);
+ final int leaderId = getRegionLeaderId(regionId);
if (leaderId != -1) {
// The migrated node is not leader, so we don't need to transfer
temporarily
@@ -936,7 +937,7 @@ public class RegionMaintainHandler {
configManager.getNodeManager().filterDataNodeThroughStatus(allowingStatus).stream()
.map(TDataNodeConfiguration::getLocation)
.collect(Collectors.toList());
- final int leaderId =
configManager.getLoadManager().getRegionLeaderMap().get(regionId);
+ final int leaderId = getRegionLeaderId(regionId);
Collections.shuffle(aliveDataNodes);
Optional<TDataNodeLocation> bestChoice = Optional.empty();
for (TDataNodeLocation aliveDataNode : aliveDataNodes) {
@@ -951,4 +952,21 @@ public class RegionMaintainHandler {
}
return bestChoice;
}
+
+ /**
+ * Get the leader of a RegionGroup, waiting for leader election if the
current cache is unknown.
+ *
+ * <p>A procedure can resume before the leader cache is rebuilt after a
ConfigNode leader switch.
+ * Waiting here gives the heartbeat cache a chance to observe the newly
elected leader while
+ * retaining the unknown-leader fallback when the election does not finish
in time.
+ */
+ private int getRegionLeaderId(TConsensusGroupId regionId) {
+ final LoadManager loadManager = configManager.getLoadManager();
+ Integer leaderId = loadManager.getRegionLeaderMap().get(regionId);
+ if (leaderId == null || leaderId == -1) {
+ loadManager.waitForRegionGroupReady(Collections.singletonList(regionId));
+ leaderId = loadManager.getRegionLeaderMap().get(regionId);
+ }
+ return leaderId == null ? -1 : leaderId;
+ }
}
diff --git
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/env/RegionMaintainHandlerTest.java
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/env/RegionMaintainHandlerTest.java
new file mode 100644
index 00000000000..8fd32922398
--- /dev/null
+++
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/env/RegionMaintainHandlerTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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.procedure.env;
+
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupId;
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType;
+import org.apache.iotdb.common.rpc.thrift.TDataNodeConfiguration;
+import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TRegionReplicaSet;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.client.IClientManager;
+import org.apache.iotdb.commons.client.sync.SyncDataNodeInternalServiceClient;
+import org.apache.iotdb.commons.cluster.NodeStatus;
+import org.apache.iotdb.confignode.client.sync.SyncDataNodeClientPool;
+import org.apache.iotdb.confignode.conf.ConfigNodeDescriptor;
+import org.apache.iotdb.confignode.manager.ConfigManager;
+import org.apache.iotdb.confignode.manager.load.LoadManager;
+import org.apache.iotdb.confignode.manager.load.balancer.RouteBalancer;
+import
org.apache.iotdb.confignode.manager.load.cache.consensus.ConsensusGroupHeartbeatSample;
+import org.apache.iotdb.confignode.manager.node.NodeManager;
+import org.apache.iotdb.confignode.manager.partition.PartitionManager;
+import org.apache.iotdb.mpp.rpc.thrift.TRegionLeaderChangeReq;
+import org.apache.iotdb.mpp.rpc.thrift.TRegionLeaderChangeResp;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.ArgumentCaptor;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.apache.iotdb.consensus.ConsensusFactory.RATIS_CONSENSUS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(Parameterized.class)
+public class RegionMaintainHandlerTest {
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> regionTypes() {
+ return Arrays.asList(
+ new Object[] {TConsensusGroupType.DataRegion},
+ new Object[] {TConsensusGroupType.SchemaRegion});
+ }
+
+ @Parameterized.Parameter public TConsensusGroupType regionType;
+
+ private final TDataNodeLocation original =
+ new TDataNodeLocation()
+ .setDataNodeId(1)
+ .setInternalEndPoint(new TEndPoint("127.0.0.1", 10730));
+ private final TDataNodeLocation coordinator = new
TDataNodeLocation().setDataNodeId(2);
+ private final TDataNodeLocation otherReplica = new
TDataNodeLocation().setDataNodeId(3);
+ private final TDataNodeLocation nonReplica = new
TDataNodeLocation().setDataNodeId(4);
+
+ private TConsensusGroupId regionId;
+ private LoadManager loadManager;
+ private RegionMaintainHandler handler;
+ private String originalConsensusProtocol;
+
+ @Before
+ public void setUp() {
+ originalConsensusProtocol =
+
ConfigNodeDescriptor.getInstance().getConf().getDataRegionConsensusProtocolClass();
+ ConfigNodeDescriptor.getInstance()
+ .getConf()
+ .setDataRegionConsensusProtocolClass(RATIS_CONSENSUS);
+
+ regionId = new TConsensusGroupId(regionType, 1);
+ ConfigManager configManager = mock(ConfigManager.class);
+ NodeManager nodeManager = mock(NodeManager.class);
+ PartitionManager partitionManager = mock(PartitionManager.class);
+ loadManager = mock(LoadManager.class);
+ when(configManager.getNodeManager()).thenReturn(nodeManager);
+ when(configManager.getPartitionManager()).thenReturn(partitionManager);
+ when(configManager.getLoadManager()).thenReturn(loadManager);
+ when(partitionManager.getAllReplicaSets())
+ .thenReturn(
+ Collections.singletonList(
+ new TRegionReplicaSet(
+ regionId, Arrays.asList(original, coordinator,
otherReplica))));
+ when(nodeManager.filterDataNodeThroughStatus(NodeStatus.Running,
NodeStatus.ReadOnly))
+ .thenReturn(
+ Arrays.asList(
+ new TDataNodeConfiguration().setLocation(original),
+ new TDataNodeConfiguration().setLocation(coordinator),
+ new TDataNodeConfiguration().setLocation(otherReplica),
+ new TDataNodeConfiguration().setLocation(nonReplica)));
+ when(loadManager.getRegionLeaderMap()).thenReturn(Collections.emptyMap());
+ handler = new RegionMaintainHandler(configManager);
+ }
+
+ @After
+ public void tearDown() {
+ ConfigNodeDescriptor.getInstance()
+ .getConf()
+ .setDataRegionConsensusProtocolClass(originalConsensusProtocol);
+ }
+
+ @Test
+ public void testFilterReplicaWithoutLeaderCache() {
+ assertEquals(
+ Optional.of(otherReplica),
+ handler.filterDataNodeWithOtherRegionReplica(
+ regionId, Arrays.asList(original, coordinator)));
+
verify(loadManager).waitForRegionGroupReady(Collections.singletonList(regionId));
+ }
+
+ @Test
+ public void testFilterReplicaWithUnknownLeader() {
+
when(loadManager.getRegionLeaderMap()).thenReturn(Collections.singletonMap(regionId,
-1));
+ assertEquals(
+ Optional.of(otherReplica),
+ handler.filterDataNodeWithOtherRegionReplica(
+ regionId, Arrays.asList(original, coordinator)));
+
verify(loadManager).waitForRegionGroupReady(Collections.singletonList(regionId));
+ }
+
+ @Test
+ public void testFilterReplicaPrefersKnownLeader() {
+ when(loadManager.getRegionLeaderMap())
+ .thenReturn(Collections.singletonMap(regionId,
otherReplica.getDataNodeId()));
+ assertEquals(
+ Optional.of(otherReplica),
+ handler.filterDataNodeWithOtherRegionReplica(regionId, original));
+ verify(loadManager,
never()).waitForRegionGroupReady(Collections.singletonList(regionId));
+ }
+
+ @Test
+ public void testFilterReplicaWithoutEligibleReplica() {
+ assertFalse(
+ handler
+ .filterDataNodeWithOtherRegionReplica(
+ regionId, Arrays.asList(original, coordinator, otherReplica))
+ .isPresent());
+ }
+
+ @Test
+ public void testTransferLeaderWithoutLeaderCache() throws Exception {
+ assertLeaderTransfer();
+ }
+
+ @Test
+ public void testTransferLeaderWithUnknownLeader() throws Exception {
+
when(loadManager.getRegionLeaderMap()).thenReturn(Collections.singletonMap(regionId,
-1));
+ assertLeaderTransfer();
+ }
+
+ @Test
+ public void testTransferLeaderWaitsForLeaderElection() throws Exception {
+ // The filter has a known leader, while the transfer lookup observes a
stale cache first.
+ when(loadManager.getRegionLeaderMap())
+ .thenReturn(Collections.singletonMap(regionId,
otherReplica.getDataNodeId()))
+ .thenReturn(Collections.emptyMap())
+ .thenReturn(Collections.singletonMap(regionId,
original.getDataNodeId()));
+ assertLeaderTransfer();
+
verify(loadManager).waitForRegionGroupReady(Collections.singletonList(regionId));
+ }
+
+ @Test
+ public void testTransferLeaderWhenCacheEntryDisappears() throws Exception {
+ // The cache can change between replica selection and the transfer's
leader lookup.
+ when(loadManager.getRegionLeaderMap())
+ .thenReturn(Collections.singletonMap(regionId, -1))
+ .thenReturn(Collections.emptyMap());
+ assertLeaderTransfer();
+ }
+
+ @SuppressWarnings("unchecked")
+ private void assertLeaderTransfer() throws Exception {
+ RouteBalancer routeBalancer = mock(RouteBalancer.class);
+ when(loadManager.getRouteBalancer()).thenReturn(routeBalancer);
+ IClientManager<TEndPoint, SyncDataNodeInternalServiceClient> clientManager
=
+ mock(IClientManager.class);
+ SyncDataNodeInternalServiceClient client =
mock(SyncDataNodeInternalServiceClient.class);
+
when(clientManager.borrowClient(original.getInternalEndPoint())).thenReturn(client);
+ TRegionLeaderChangeReq request = new TRegionLeaderChangeReq(regionId,
otherReplica);
+ long timestamp = 123L;
+ when(client.changeRegionLeader(request))
+ .thenReturn(
+ new TRegionLeaderChangeResp(
+ new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()),
timestamp));
+
+ SyncDataNodeClientPool clientPool = SyncDataNodeClientPool.getInstance();
+ Field clientManagerField =
SyncDataNodeClientPool.class.getDeclaredField("clientManager");
+ clientManagerField.setAccessible(true);
+ Object originalClientManager = clientManagerField.get(clientPool);
+ try {
+ clientManagerField.set(clientPool, clientManager);
+ handler.transferRegionLeader(regionId, original, coordinator);
+
+ verify(client).changeRegionLeader(request);
+ ArgumentCaptor<Map<TConsensusGroupId, ConsensusGroupHeartbeatSample>>
cacheUpdate =
+ ArgumentCaptor.forClass(Map.class);
+
verify(loadManager).forceUpdateConsensusGroupCache(cacheUpdate.capture());
+ assertEquals(1, cacheUpdate.getValue().size());
+ ConsensusGroupHeartbeatSample sample =
cacheUpdate.getValue().get(regionId);
+ assertEquals(otherReplica.getDataNodeId(), sample.getLeaderId());
+ assertEquals(timestamp, sample.getSampleLogicalTimestamp());
+ verify(routeBalancer).balanceRegionLeaderAndPriority();
+ } finally {
+ clientManagerField.set(clientPool, originalClientManager);
+ }
+ }
+}