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 847cc6d1848 Fix ConfigNode leader transfer retries during removal 
(#18348)
847cc6d1848 is described below

commit 847cc6d1848cedb50d838378c3841d4b8afee0f9
Author: Yongzao <[email protected]>
AuthorDate: Wed Jul 29 17:15:06 2026 +0800

    Fix ConfigNode leader transfer retries during removal (#18348)
---
 .../IoTDBRemoveConfigNodeITFramework.java          |  20 ++-
 .../iotdb/confignode/i18n/ManagerMessages.java     |   3 +
 .../iotdb/confignode/i18n/ManagerMessages.java     |   3 +
 .../iotdb/confignode/manager/node/NodeManager.java |  79 +++++++--
 .../confignode/manager/node/NodeManagerTest.java   | 189 +++++++++++++++++++++
 5 files changed, 268 insertions(+), 26 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/removeconfignode/IoTDBRemoveConfigNodeITFramework.java
 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/removeconfignode/IoTDBRemoveConfigNodeITFramework.java
index 6112fea7f52..54552ad4867 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/removeconfignode/IoTDBRemoveConfigNodeITFramework.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/removeconfignode/IoTDBRemoveConfigNodeITFramework.java
@@ -19,11 +19,11 @@
 
 package org.apache.iotdb.confignode.it.removeconfignode;
 
-import org.apache.iotdb.commons.client.sync.SyncConfigNodeIServiceClient;
 import org.apache.iotdb.commons.schema.column.ColumnHeaderConstant;
 import org.apache.iotdb.confignode.it.removedatanode.SQLModel;
 import org.apache.iotdb.consensus.ConsensusFactory;
 import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.env.cluster.node.ConfigNodeWrapper;
 import org.apache.iotdb.itbase.exception.InconsistentDataException;
 import org.apache.iotdb.jdbc.IoTDBSQLException;
 import org.apache.iotdb.relational.it.query.old.aligned.TableUtils;
@@ -97,9 +97,7 @@ public class IoTDBRemoveConfigNodeITFramework {
     EnvFactory.getEnv().initClusterEnvironment(configNodeNum, dataNodeNum);
 
     try (final Connection connection = 
makeItCloseQuietly(getConnectionWithSQLType(model));
-        final Statement statement = 
makeItCloseQuietly(connection.createStatement());
-        SyncConfigNodeIServiceClient client =
-            (SyncConfigNodeIServiceClient) 
EnvFactory.getEnv().getLeaderConfigNodeConnection()) {
+        final Statement statement = 
makeItCloseQuietly(connection.createStatement())) {
 
       if (SQLModel.TABLE_MODEL_SQL.equals(model)) {
         // Insert data in table model
@@ -120,14 +118,18 @@ public class IoTDBRemoveConfigNodeITFramework {
 
       // Get all config nodes
       ResultSet result = statement.executeQuery(SHOW_CONFIGNODES);
-      Set<Integer> allConfigNodeId = new HashSet<>();
+      int removeConfigNodeId = -1;
+      ConfigNodeWrapper leaderConfigNode =
+          
EnvFactory.getEnv().getConfigNodeWrapper(EnvFactory.getEnv().getLeaderConfigNodeIndex());
       while (result.next()) {
-        allConfigNodeId.add(result.getInt(ColumnHeaderConstant.NODE_ID));
+        int configNodeId = result.getInt(ColumnHeaderConstant.NODE_ID);
+        if 
(leaderConfigNode.getIp().equals(result.getString(ColumnHeaderConstant.INTERNAL_ADDRESS))
+            && leaderConfigNode.getPort() == 
result.getInt(ColumnHeaderConstant.INTERNAL_PORT)) {
+          removeConfigNodeId = configNodeId;
+        }
       }
+      Assert.assertNotEquals(-1, removeConfigNodeId);
 
-      AtomicReference<SyncConfigNodeIServiceClient> clientRef = new 
AtomicReference<>(client);
-
-      int removeConfigNodeId = allConfigNodeId.iterator().next();
       String removeConfigNodeSQL = generateRemoveString(removeConfigNodeId);
       LOGGER.info("Remove ConfigNodes SQL: {}", removeConfigNodeSQL);
       try {
diff --git 
a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java
 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java
index 7028e97be7e..13bfaf6de8a 100644
--- 
a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java
+++ 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java
@@ -551,6 +551,9 @@ public final class ManagerMessages {
   public static final String 
REMOVE_CONFIGNODE_FAILED_BECAUSE_THE_CONFIGNODE_NOT_IN_CURRENT_CLUSTER = 
"Remove ConfigNode failed because the ConfigNode not in current Cluster.";
   public static final String SUCCESSFULLY_REMOVE_CONFIGNODE = "Successfully 
remove confignode.";
   public static final String 
REMOVE_CONFIGNODE_FAILED_BECAUSE_TRANSFER_CONFIGNODE_LEADER_FAILED = "Remove 
ConfigNode failed because transfer ConfigNode leader failed.";
+  public static final String 
LOG_FAILED_TO_TRANSFER_CONFIGNODE_LEADER_FROM_ARG_TO_ARG_TRYING_ANOTHER_CANDIDATE_BA922E92
 = "Failed to transfer ConfigNode leader from {} to {}, trying another 
candidate.";
+  public static final String 
LOG_COULD_NOT_CONFIRM_A_CONFIGNODE_LEADER_OTHER_THAN_ARG_AFTER_ATTEMPTING_TO_TRANSFER_LEADERSHIP_TO_ARG_TRYING_ANOTHER_CANDIDATE_91EF68C1
 = "Could not confirm a ConfigNode leader other than {} after attempting to 
transfer leadership to {}, trying another candidate.";
+  public static final String 
LOG_STOPPED_RETRYING_CONFIGNODE_LEADER_TRANSFER_BECAUSE_THE_REMAINING_RPC_TIMEOUT_IS_INSUFFICIENT_6429A49C
 = "Stopped retrying ConfigNode leader transfer because the remaining RPC 
timeout is insufficient.";
 
   private ManagerMessages() {}
   // 
---------------------------------------------------------------------------
diff --git 
a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java
 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java
index 77c005cddf4..42aa21f46ae 100644
--- 
a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java
+++ 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java
@@ -546,6 +546,9 @@ public final class ManagerMessages {
   public static final String 
REMOVE_CONFIGNODE_FAILED_BECAUSE_THE_CONFIGNODE_NOT_IN_CURRENT_CLUSTER = "移除 
ConfigNode 失败,该 ConfigNode 不在当前集群中。";
   public static final String SUCCESSFULLY_REMOVE_CONFIGNODE = "成功移除 
ConfigNode。";
   public static final String 
REMOVE_CONFIGNODE_FAILED_BECAUSE_TRANSFER_CONFIGNODE_LEADER_FAILED = "移除 
ConfigNode 失败,转移 ConfigNode leader 失败。";
+  public static final String 
LOG_FAILED_TO_TRANSFER_CONFIGNODE_LEADER_FROM_ARG_TO_ARG_TRYING_ANOTHER_CANDIDATE_BA922E92
 = "将 ConfigNode leader 从 {} 转移到 {} 失败,将尝试其他候选节点。";
+  public static final String 
LOG_COULD_NOT_CONFIRM_A_CONFIGNODE_LEADER_OTHER_THAN_ARG_AFTER_ATTEMPTING_TO_TRANSFER_LEADERSHIP_TO_ARG_TRYING_ANOTHER_CANDIDATE_91EF68C1
 = "无法确认存在除 ConfigNode {} 以外的 leader;此前尝试的转移目标为 {},将尝试其他候选节点。";
+  public static final String 
LOG_STOPPED_RETRYING_CONFIGNODE_LEADER_TRANSFER_BECAUSE_THE_REMAINING_RPC_TIMEOUT_IS_INSUFFICIENT_6429A49C
 = "剩余 RPC 超时时间不足,停止重试 ConfigNode leader 转移。";
 
   private ManagerMessages() {}
   // 
---------------------------------------------------------------------------
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/node/NodeManager.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/node/NodeManager.java
index 293f1942ff5..64d4cf741e1 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/node/NodeManager.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/node/NodeManager.java
@@ -104,6 +104,7 @@ import 
org.apache.iotdb.confignode.rpc.thrift.TSetDataNodeStatusReq;
 import org.apache.iotdb.consensus.common.DataSet;
 import org.apache.iotdb.consensus.common.Peer;
 import org.apache.iotdb.consensus.exception.ConsensusException;
+import org.apache.iotdb.consensus.exception.RatisRequestFailedException;
 import org.apache.iotdb.mpp.rpc.thrift.TKillQueryInstanceReq;
 import org.apache.iotdb.rpc.RpcUtils;
 import org.apache.iotdb.rpc.TSStatusCode;
@@ -985,30 +986,74 @@ public class NodeManager {
 
   private TSStatus transferLeader(
       RemoveConfigNodePlan removeConfigNodePlan, ConsensusGroupId groupId) {
-    Optional<TConfigNodeLocation> optional =
+    final TConfigNodeLocation removedConfigNode = 
removeConfigNodePlan.getConfigNodeLocation();
+    final List<TConfigNodeLocation> newLeaderCandidates =
         filterConfigNodeThroughStatus(NodeStatus.Running).stream()
-            .filter(e -> 
!e.equals(removeConfigNodePlan.getConfigNodeLocation()))
-            .findAny();
-    TConfigNodeLocation newLeader = null;
-    if (optional.isPresent()) {
-      newLeader = optional.get();
-    } else {
+            .filter(configNode -> !configNode.equals(removedConfigNode))
+            
.sorted(Comparator.comparingInt(TConfigNodeLocation::getConfigNodeId))
+            .collect(Collectors.toList());
+    if (newLeaderCandidates.isEmpty()) {
       return new TSStatus(TSStatusCode.TRANSFER_LEADER_ERROR.getStatusCode())
           .setMessage(
               ManagerMessages
                   
.MESSAGE_TRANSFER_CONFIGNODE_LEADER_FAILED_BECAUSE_CAN_NOT_FIND_ANY_RUNNING_1FE4F96D);
     }
-    try {
-      getConsensusManager()
-          .getConsensusImpl()
-          .transferLeader(
-              groupId,
-              new Peer(groupId, newLeader.getConfigNodeId(), 
newLeader.getConsensusEndPoint()));
-    } catch (ConsensusException e) {
-      return new TSStatus(TSStatusCode.REMOVE_CONFIGNODE_ERROR.getStatusCode())
-          .setMessage(
-              
ManagerMessages.REMOVE_CONFIGNODE_FAILED_BECAUSE_TRANSFER_CONFIGNODE_LEADER_FAILED);
+
+    final long retryDeadline =
+        System.nanoTime()
+            + 
TimeUnit.MILLISECONDS.toNanos(COMMON_CONFIG.getCnConnectionTimeoutInMS());
+    final long transferLeaderTimeout =
+        TimeUnit.MILLISECONDS.toNanos(CONF.getRatisTransferLeaderTimeoutMs());
+
+    for (int i = 0; i < newLeaderCandidates.size(); i++) {
+      if (i > 0 && retryDeadline - System.nanoTime() <= transferLeaderTimeout) 
{
+        LOGGER.warn(
+            ManagerMessages
+                
.LOG_STOPPED_RETRYING_CONFIGNODE_LEADER_TRANSFER_BECAUSE_THE_REMAINING_RPC_TIMEOUT_IS_INSUFFICIENT_6429A49C);
+        break;
+      }
+
+      final TConfigNodeLocation newLeaderCandidate = 
newLeaderCandidates.get(i);
+      try {
+        getConsensusManager()
+            .getConsensusImpl()
+            .transferLeader(
+                groupId,
+                new Peer(
+                    groupId,
+                    newLeaderCandidate.getConfigNodeId(),
+                    newLeaderCandidate.getConsensusEndPoint()));
+      } catch (RatisRequestFailedException e) {
+        LOGGER.warn(
+            ManagerMessages
+                
.LOG_FAILED_TO_TRANSFER_CONFIGNODE_LEADER_FROM_ARG_TO_ARG_TRYING_ANOTHER_CANDIDATE_BA922E92,
+            removedConfigNode.getConfigNodeId(),
+            newLeaderCandidate.getConfigNodeId(),
+            e);
+      } catch (ConsensusException e) {
+        return new 
TSStatus(TSStatusCode.REMOVE_CONFIGNODE_ERROR.getStatusCode())
+            .setMessage(
+                
ManagerMessages.REMOVE_CONFIGNODE_FAILED_BECAUSE_TRANSFER_CONFIGNODE_LEADER_FAILED);
+      }
+
+      final TConfigNodeLocation actualLeader = 
getConsensusManager().getLeaderLocation();
+      if (actualLeader != null && !actualLeader.equals(removedConfigNode)) {
+        return buildConfigNodeLeaderTransferredStatus(actualLeader);
+      }
+
+      LOGGER.warn(
+          ManagerMessages
+              
.LOG_COULD_NOT_CONFIRM_A_CONFIGNODE_LEADER_OTHER_THAN_ARG_AFTER_ATTEMPTING_TO_TRANSFER_LEADERSHIP_TO_ARG_TRYING_ANOTHER_CANDIDATE_91EF68C1,
+          removedConfigNode.getConfigNodeId(),
+          newLeaderCandidate.getConfigNodeId());
     }
+
+    return new TSStatus(TSStatusCode.REMOVE_CONFIGNODE_ERROR.getStatusCode())
+        .setMessage(
+            
ManagerMessages.REMOVE_CONFIGNODE_FAILED_BECAUSE_TRANSFER_CONFIGNODE_LEADER_FAILED);
+  }
+
+  private TSStatus buildConfigNodeLeaderTransferredStatus(TConfigNodeLocation 
newLeader) {
     return new TSStatus(TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode())
         .setRedirectNode(newLeader.getInternalEndPoint())
         .setMessage(
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/node/NodeManagerTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/node/NodeManagerTest.java
new file mode 100644
index 00000000000..f39ecd46d5c
--- /dev/null
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/node/NodeManagerTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.manager.node;
+
+import org.apache.iotdb.common.rpc.thrift.TConfigNodeLocation;
+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.conf.CommonDescriptor;
+import org.apache.iotdb.confignode.conf.ConfigNodeDescriptor;
+import 
org.apache.iotdb.confignode.consensus.request.write.confignode.RemoveConfigNodePlan;
+import org.apache.iotdb.confignode.manager.IManager;
+import org.apache.iotdb.confignode.manager.consensus.ConsensusManager;
+import org.apache.iotdb.confignode.manager.load.LoadManager;
+import org.apache.iotdb.confignode.persistence.node.NodeInfo;
+import org.apache.iotdb.consensus.IConsensus;
+import org.apache.iotdb.consensus.common.Peer;
+import org.apache.iotdb.consensus.exception.ConsensusException;
+import org.apache.iotdb.consensus.exception.RatisRequestFailedException;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class NodeManagerTest {
+
+  private final TConfigNodeLocation removedConfigNode = 
newConfigNodeLocation(0);
+  private final TConfigNodeLocation firstCandidate = newConfigNodeLocation(1);
+  private final TConfigNodeLocation secondCandidate = newConfigNodeLocation(2);
+  private final List<TConfigNodeLocation> configNodes =
+      Arrays.asList(removedConfigNode, firstCandidate, secondCandidate);
+
+  private IConsensus consensus;
+  private ConsensusManager consensusManager;
+  private NodeManager nodeManager;
+  private int originalCnConnectionTimeout;
+  private int originalTransferLeaderTimeout;
+
+  @Before
+  public void setUp() {
+    originalCnConnectionTimeout =
+        
CommonDescriptor.getInstance().getConfig().getCnConnectionTimeoutInMS();
+    originalTransferLeaderTimeout =
+        
ConfigNodeDescriptor.getInstance().getConf().getRatisTransferLeaderTimeoutMs();
+    
CommonDescriptor.getInstance().getConfig().setCnConnectionTimeoutInMS(60_000);
+    
ConfigNodeDescriptor.getInstance().getConf().setRatisTransferLeaderTimeoutMs(1_000);
+
+    consensus = Mockito.mock(IConsensus.class);
+    consensusManager = Mockito.mock(ConsensusManager.class);
+    IManager configManager = Mockito.mock(IManager.class);
+    NodeInfo nodeInfo = Mockito.mock(NodeInfo.class);
+    LoadManager loadManager = Mockito.mock(LoadManager.class);
+
+    
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+    Mockito.when(configManager.getLoadManager()).thenReturn(loadManager);
+    Mockito.when(consensusManager.getConsensusImpl()).thenReturn(consensus);
+    Mockito.when(consensusManager.getConsensusGroupId())
+        .thenReturn(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID);
+    Mockito.when(nodeInfo.getRegisteredConfigNodes()).thenReturn(configNodes);
+    
Mockito.when(loadManager.filterConfigNodeThroughStatus(Mockito.any(NodeStatus[].class)))
+        .thenReturn(
+            configNodes.stream()
+                .map(TConfigNodeLocation::getConfigNodeId)
+                .collect(Collectors.toList()));
+    
Mockito.when(nodeInfo.getRegisteredConfigNodes(Mockito.anyList())).thenReturn(configNodes);
+
+    nodeManager = new NodeManager(configManager, nodeInfo);
+  }
+
+  @After
+  public void tearDown() {
+    CommonDescriptor.getInstance()
+        .getConfig()
+        .setCnConnectionTimeoutInMS(originalCnConnectionTimeout);
+    ConfigNodeDescriptor.getInstance()
+        .getConf()
+        .setRatisTransferLeaderTimeoutMs(originalTransferLeaderTimeout);
+  }
+
+  @Test
+  public void transferLeaderShouldTryAnotherCandidateAfterTransientFailure() 
throws Exception {
+    Mockito.when(consensusManager.getLeaderLocation())
+        .thenReturn(removedConfigNode, removedConfigNode, secondCandidate);
+    Mockito.doThrow(new RatisRequestFailedException(new Exception("transfer 
failed")))
+        .doNothing()
+        .when(consensus)
+        .transferLeader(
+            Mockito.eq(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID), 
Mockito.any(Peer.class));
+
+    TSStatus status =
+        nodeManager.checkConfigNodeBeforeRemove(new 
RemoveConfigNodePlan(removedConfigNode));
+
+    Assert.assertEquals(TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode(), 
status.getCode());
+    Assert.assertEquals(secondCandidate.getInternalEndPoint(), 
status.getRedirectNode());
+    ArgumentCaptor<Peer> peerCaptor = ArgumentCaptor.forClass(Peer.class);
+    Mockito.verify(consensus, Mockito.times(2))
+        .transferLeader(
+            Mockito.eq(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID), 
peerCaptor.capture());
+    Assert.assertEquals(
+        Arrays.asList(firstCandidate.getConfigNodeId(), 
secondCandidate.getConfigNodeId()),
+        
peerCaptor.getAllValues().stream().map(Peer::getNodeId).collect(Collectors.toList()));
+  }
+
+  @Test
+  public void transferLeaderShouldRedirectToActualLeaderAfterFailedResponse() 
throws Exception {
+    Mockito.when(consensusManager.getLeaderLocation())
+        .thenReturn(removedConfigNode, secondCandidate);
+    Mockito.doThrow(new RatisRequestFailedException(new Exception("transfer 
failed")))
+        .when(consensus)
+        .transferLeader(
+            Mockito.eq(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID), 
Mockito.any(Peer.class));
+
+    TSStatus status =
+        nodeManager.checkConfigNodeBeforeRemove(new 
RemoveConfigNodePlan(removedConfigNode));
+
+    Assert.assertEquals(TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode(), 
status.getCode());
+    Assert.assertEquals(secondCandidate.getInternalEndPoint(), 
status.getRedirectNode());
+    Mockito.verify(consensus)
+        .transferLeader(
+            Mockito.eq(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID), 
Mockito.any(Peer.class));
+  }
+
+  @Test
+  public void transferLeaderShouldFailAfterAllCandidatesFail() throws 
Exception {
+    Mockito.when(consensusManager.getLeaderLocation())
+        .thenReturn(removedConfigNode, removedConfigNode, removedConfigNode);
+    Mockito.doThrow(new RatisRequestFailedException(new Exception("transfer 
failed")))
+        .when(consensus)
+        .transferLeader(
+            Mockito.eq(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID), 
Mockito.any(Peer.class));
+
+    TSStatus status =
+        nodeManager.checkConfigNodeBeforeRemove(new 
RemoveConfigNodePlan(removedConfigNode));
+
+    Assert.assertEquals(TSStatusCode.REMOVE_CONFIGNODE_ERROR.getStatusCode(), 
status.getCode());
+    Mockito.verify(consensus, Mockito.times(2))
+        .transferLeader(
+            Mockito.eq(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID), 
Mockito.any(Peer.class));
+  }
+
+  @Test
+  public void transferLeaderShouldNotRetryNonRatisFailure() throws Exception {
+    
Mockito.when(consensusManager.getLeaderLocation()).thenReturn(removedConfigNode);
+    Mockito.doThrow(new ConsensusException("non-retriable failure"))
+        .when(consensus)
+        .transferLeader(
+            Mockito.eq(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID), 
Mockito.any(Peer.class));
+
+    TSStatus status =
+        nodeManager.checkConfigNodeBeforeRemove(new 
RemoveConfigNodePlan(removedConfigNode));
+
+    Assert.assertEquals(TSStatusCode.REMOVE_CONFIGNODE_ERROR.getStatusCode(), 
status.getCode());
+    Mockito.verify(consensus)
+        .transferLeader(
+            Mockito.eq(ConsensusManager.DEFAULT_CONSENSUS_GROUP_ID), 
Mockito.any(Peer.class));
+  }
+
+  private static TConfigNodeLocation newConfigNodeLocation(int configNodeId) {
+    return new TConfigNodeLocation(
+        configNodeId,
+        new TEndPoint("127.0.0.1", 10710 + configNodeId),
+        new TEndPoint("127.0.0.1", 10720 + configNodeId));
+  }
+}

Reply via email to