Skip to content

Commit c462a97

Browse files
Make IoTConsensus peer-list reset retries best effort (#18929)
Finish node removal after bounded peer-list reset retries are exhausted. Keep metadata failures fatal and simplify caller-controlled retry handling. Cover exhaustion paths and adapt regression fixtures to targeted region lookups.
1 parent 08380be commit c462a97

6 files changed

Lines changed: 124 additions & 59 deletions

File tree

‎iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -853,8 +853,6 @@ public final class ProcedureMessages {
853853
public static final String
854854
LOG_FAILED_TO_RESET_PEER_LIST_FOR_ARG_ON_DATANODE_ARG_TO_ARG_ARG_C8D872B4 =
855855
"Failed to reset peer list for {} on DataNode {} to {}: {}";
856-
public static final String LOG_PIDARG_REMOVEREGION_STATE_ARG_FAILED_RETRY_ARG_ARG_IN_ARG_MS_41B8ECB5 =
857-
"[pid{}][RemoveRegion] state {} failed, retry {}/{} in {} ms.";
858856
public static final String
859857
EXCEPTION_FAILED_TO_FINALIZE_REGION_PEER_REMOVAL_FOR_ARG_AT_STATE_ARG_AFTER_ARG_ATTEMPTS_382EA168 =
860858
"Failed to finalize Region peer removal for %s at state %s after %d attempts.";

‎iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -812,8 +812,6 @@ public final class ProcedureMessages {
812812
public static final String
813813
LOG_FAILED_TO_RESET_PEER_LIST_FOR_ARG_ON_DATANODE_ARG_TO_ARG_ARG_C8D872B4 =
814814
"将 {} 在 DataNode {} 上的 peer 列表重置为 {} 失败:{}";
815-
public static final String LOG_PIDARG_REMOVEREGION_STATE_ARG_FAILED_RETRY_ARG_ARG_IN_ARG_MS_41B8ECB5 =
816-
"[pid{}][RemoveRegion] 状态 {} 失败,第 {}/{} 次重试将在 {} 毫秒后进行。";
817815
public static final String
818816
EXCEPTION_FAILED_TO_FINALIZE_REGION_PEER_REMOVAL_FOR_ARG_AT_STATE_ARG_AFTER_ARG_ATTEMPTS_382EA168 =
819817
"Region %s 的 peer 移除收尾在状态 %s 尝试 %d 次后失败。";

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionPeerProcedure.java‎

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import org.slf4j.Logger;
4040
import org.slf4j.LoggerFactory;
4141

42+
import javax.annotation.Nullable;
43+
4244
import java.io.DataOutputStream;
4345
import java.io.IOException;
4446
import java.nio.ByteBuffer;
@@ -198,7 +200,15 @@ protected Flow executeFromState(ConfigNodeProcedureEnv env, RemoveRegionPeerStat
198200
case REMOVE_REGION_LOCATION_CACHE:
199201
tsStatus = handler.removeRegionLocation(regionId, targetDataNode);
200202
if (tsStatus == null || tsStatus.getCode() != SUCCESS_STATUS.getStatusCode()) {
201-
return retryStep(state);
203+
return retryStep(
204+
state,
205+
new ProcedureException(
206+
String.format(
207+
ProcedureMessages
208+
.EXCEPTION_FAILED_TO_FINALIZE_REGION_PEER_REMOVAL_FOR_ARG_AT_STATE_ARG_AFTER_ARG_ATTEMPTS_382EA168,
209+
regionId,
210+
state,
211+
getCycles() + 1)));
202212
}
203213
if (!handler.isIoTConsensusV1DataRegion(regionId)) {
204214
return finishRemoval(state);
@@ -208,45 +218,36 @@ protected Flow executeFromState(ConfigNodeProcedureEnv env, RemoveRegionPeerStat
208218
break;
209219
case RESET_IOT_CONSENSUS_V1_PEER_LIST:
210220
if (!handler.resetPeerListAfterRemoveForIoTConsensusV1(regionId, targetDataNode)) {
211-
return retryStep(state);
221+
// The replica metadata is already committed. Peer list reconciliation is best effort.
222+
return retryStep(state, null);
212223
}
213224
return finishRemoval(state);
214225
default:
215226
throw new ProcedureException(ProcedureMessages.UNSUPPORTED_STATE + state.name());
216227
}
217228
} catch (Exception e) {
218229
LOGGER.error(ProcedureMessages.REMOVEREGIONPEER_STATE_FAILED, state, e);
219-
if (state == REMOVE_REGION_LOCATION_CACHE || state == RESET_IOT_CONSENSUS_V1_PEER_LIST) {
220-
return retryStep(state);
230+
if (state == REMOVE_REGION_LOCATION_CACHE) {
231+
return retryStep(state, new ProcedureException(e));
232+
}
233+
if (state == RESET_IOT_CONSENSUS_V1_PEER_LIST) {
234+
return retryStep(state, null);
221235
}
222236
return Flow.NO_MORE_STATE;
223237
}
224238
LOGGER.info(ProcedureMessages.PID_REMOVEREGION_STATE_SUCCESS, getProcId(), state);
225239
return Flow.HAS_MORE_STATE;
226240
}
227241

228-
private Flow retryStep(RemoveRegionPeerState state) {
242+
private Flow retryStep(
243+
RemoveRegionPeerState state, @Nullable ProcedureException failureOnExhaustion) {
229244
if (getCycles() >= MAX_FINALIZATION_RETRY) {
230-
ProcedureException failure =
231-
new ProcedureException(
232-
String.format(
233-
ProcedureMessages
234-
.EXCEPTION_FAILED_TO_FINALIZE_REGION_PEER_REMOVAL_FOR_ARG_AT_STATE_ARG_AFTER_ARG_ATTEMPTS_382EA168,
235-
regionId,
236-
state,
237-
getCycles() + 1));
238-
LOGGER.error(ProcedureMessages.REMOVEREGIONPEER_STATE_FAILED, state, failure);
239-
setFailure(failure);
245+
if (failureOnExhaustion != null) {
246+
setFailure(failureOnExhaustion);
247+
}
240248
return Flow.NO_MORE_STATE;
241249
}
242250
setNextState(state);
243-
LOGGER.warn(
244-
ProcedureMessages.LOG_PIDARG_REMOVEREGION_STATE_ARG_FAILED_RETRY_ARG_ARG_IN_ARG_MS_41B8ECB5,
245-
getProcId(),
246-
state,
247-
getCycles() + 1,
248-
MAX_FINALIZATION_RETRY,
249-
FINALIZATION_RETRY_INTERVAL_MS);
250251
try {
251252
Thread.sleep(FINALIZATION_RETRY_INTERVAL_MS);
252253
} catch (InterruptedException e) {

‎iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/node/RemoveDataNodesProcedureTest.java‎

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,19 @@ public void serDeTest() throws IOException {
137137
}
138138

139139
@Test
140-
public void metadataRetryExhaustionCleansUpNodeRemoval() throws Exception {
141-
assertFinalizationFailureCleanup(RemoveRegionPeerState.REMOVE_REGION_LOCATION_CACHE, false);
140+
public void metadataRetryExhaustionRetriesFailedNodeStatusCleanup() throws Exception {
141+
assertFinalizationExhaustionCleanup(RemoveRegionPeerState.REMOVE_REGION_LOCATION_CACHE, true);
142142
}
143143

144144
@Test
145-
public void peerListRetryExhaustionRetriesFailedNodeStatusCleanup() throws Exception {
146-
assertFinalizationFailureCleanup(RemoveRegionPeerState.RESET_IOT_CONSENSUS_V1_PEER_LIST, true);
145+
public void peerListRetryExhaustionCompletesNodeRemoval() throws Exception {
146+
assertFinalizationExhaustionCleanup(
147+
RemoveRegionPeerState.RESET_IOT_CONSENSUS_V1_PEER_LIST, false);
147148
}
148149

149-
private void assertFinalizationFailureCleanup(
150+
private void assertFinalizationExhaustionCleanup(
150151
RemoveRegionPeerState failingState, boolean failFirstStatusRestoration) throws Exception {
152+
boolean metadataFailure = failingState == RemoveRegionPeerState.REMOVE_REGION_LOCATION_CACHE;
151153
TDataNodeLocation runningNode = fromDataNodeLocation;
152154
TDataNodeLocation readOnlyNode = location(12);
153155
TDataNodeLocation drainedNode = location(13);
@@ -178,10 +180,12 @@ private void assertFinalizationFailureCleanup(
178180
"root.remove",
179181
new TRegionReplicaSet(
180182
consensusGroupId, new ArrayList<>(Arrays.asList(runningNode, retainedNode))));
181-
groups.addRegionGroup(
182-
"root.remove",
183-
new TRegionReplicaSet(
184-
pendingRunningRegion, new ArrayList<>(Arrays.asList(runningNode, retainedNode))));
183+
if (metadataFailure) {
184+
groups.addRegionGroup(
185+
"root.remove",
186+
new TRegionReplicaSet(
187+
pendingRunningRegion, new ArrayList<>(Arrays.asList(runningNode, retainedNode))));
188+
}
185189
groups.addRegionGroup(
186190
"root.remove",
187191
new TRegionReplicaSet(
@@ -229,7 +233,7 @@ private void assertFinalizationFailureCleanup(
229233
when(regionHandler.removeRegionLocation(consensusGroupId, runningNode))
230234
.thenAnswer(
231235
ignored ->
232-
failingState == RemoveRegionPeerState.REMOVE_REGION_LOCATION_CACHE
236+
metadataFailure
233237
? new TSStatus(TSStatusCode.MIGRATE_REGION_ERROR.getStatusCode())
234238
: partitionInfo.removeRegionLocation(
235239
new RemoveRegionLocationPlan(consensusGroupId, runningNode)));
@@ -302,53 +306,71 @@ private void assertFinalizationFailureCleanup(
302306
new RemoveDataNodesProcedure(removalTargets, originalStatuses);
303307
long rootId = executor.submitProcedure(root);
304308
ProcedureTestUtil.waitForProcedure(executor, rootId);
305-
Assert.assertTrue("Node removal must finish failure cleanup", executor.isFinished(rootId));
306-
Assert.assertEquals(ProcedureState.ROLLEDBACK, root.getState());
307-
Assert.assertFalse(root.isSuccess());
308-
RemoveRegionPeerProcedure failedChild =
309+
Assert.assertTrue("Node removal must finish cleanup", executor.isFinished(rootId));
310+
RemoveRegionPeerProcedure removalChild =
309311
store.recorded.values().stream()
310312
.filter(RemoveRegionPeerProcedure.class::isInstance)
311313
.map(RemoveRegionPeerProcedure.class::cast)
312314
.findFirst()
313315
.get();
314-
Procedure<?> migrationParent = store.recorded.get(failedChild.getParentProcId());
316+
Procedure<?> migrationParent = store.recorded.get(removalChild.getParentProcId());
315317
Assert.assertTrue(migrationParent instanceof RegionMigrateProcedure);
316318
Assert.assertEquals(rootId, migrationParent.getParentProcId());
317-
Assert.assertSame(failedChild.getException(), root.getException());
318-
Assert.assertTrue(root.getException().getMessage().contains(failingState.name()));
319-
if (failingState == RemoveRegionPeerState.REMOVE_REGION_LOCATION_CACHE) {
319+
if (metadataFailure) {
320+
Assert.assertEquals(ProcedureState.ROLLEDBACK, root.getState());
321+
Assert.assertFalse(root.isSuccess());
322+
Assert.assertSame(removalChild.getException(), root.getException());
323+
Assert.assertTrue(root.getException().getMessage().contains(failingState.name()));
320324
verify(regionHandler, times(4)).removeRegionLocation(consensusGroupId, runningNode);
321325
verify(regionHandler, never()).resetPeerListAfterRemoveForIoTConsensusV1(any(), any());
322326
} else {
327+
Assert.assertTrue(root.isSuccess());
328+
Assert.assertTrue(migrationParent.isSuccess());
329+
Assert.assertTrue(removalChild.isSuccess());
330+
Assert.assertNull(root.getException());
331+
Assert.assertNull(migrationParent.getException());
332+
Assert.assertNull(removalChild.getException());
323333
verify(regionHandler).removeRegionLocation(consensusGroupId, runningNode);
324334
verify(regionHandler, times(4))
325335
.resetPeerListAfterRemoveForIoTConsensusV1(consensusGroupId, runningNode);
326336
}
327337

328-
// Failed children bypass STOP_DATA_NODE: root rollback must still finalize node statuses.
329-
Assert.assertEquals(NodeStatus.Running, currentStatuses.get(runningNode.getDataNodeId()));
338+
// Metadata failures use rollback cleanup; exhausted peer-list resets use normal
339+
// STOP_DATA_NODE.
340+
if (metadataFailure) {
341+
Assert.assertEquals(NodeStatus.Running, currentStatuses.get(runningNode.getDataNodeId()));
342+
}
330343
Assert.assertEquals(NodeStatus.ReadOnly, currentStatuses.get(readOnlyNode.getDataNodeId()));
331-
Assert.assertTrue(registeredNodes.contains(runningNode.getDataNodeId()));
344+
Assert.assertEquals(metadataFailure, registeredNodes.contains(runningNode.getDataNodeId()));
332345
Assert.assertTrue(registeredNodes.contains(readOnlyNode.getDataNodeId()));
333346
Assert.assertFalse(registeredNodes.contains(drainedNode.getDataNodeId()));
334-
Assert.assertEquals(Collections.singleton(drainedNode.getDataNodeId()), stoppedNodes);
347+
Assert.assertEquals(
348+
metadataFailure
349+
? Collections.singleton(drainedNode.getDataNodeId())
350+
: Set.of(runningNode.getDataNodeId(), drainedNode.getDataNodeId()),
351+
stoppedNodes);
335352
Assert.assertEquals(failFirstStatusRestoration ? 2 : 1, restorationAttempts.get());
336353
if (failFirstStatusRestoration) {
337354
// A node-lock wakeup must not bypass the five-second rollback retry timeout.
338355
Assert.assertTrue(
339356
"Status restoration must wait for the rollback retry timeout",
340357
restorationRetryDelay.get() >= TimeUnit.SECONDS.toNanos(4));
341358
}
342-
verify(nodeHandler).broadcastDataNodeStatusChange(Arrays.asList(runningNode, readOnlyNode));
359+
verify(nodeHandler)
360+
.broadcastDataNodeStatusChange(
361+
metadataFailure
362+
? Arrays.asList(runningNode, readOnlyNode)
363+
: Collections.singletonList(readOnlyNode));
343364

344365
// Cleanup must not undo the completed add peer or a committed removal.
345366
Assert.assertTrue(
346367
partitionManager.isDataNodeContainsRegion(
347368
toDataNodeLocation.getDataNodeId(), consensusGroupId));
348369
Assert.assertEquals(
349-
failingState == RemoveRegionPeerState.REMOVE_REGION_LOCATION_CACHE,
370+
metadataFailure,
350371
partitionManager.isDataNodeContainsRegion(runningNode.getDataNodeId(), consensusGroupId));
351-
Assert.assertTrue(
372+
Assert.assertEquals(
373+
metadataFailure,
352374
partitionManager.isDataNodeContainsRegion(
353375
runningNode.getDataNodeId(), pendingRunningRegion));
354376
Assert.assertTrue(

‎iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/region/RemovePeerRestartConsensusTest.java‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,14 @@ public void removalProcedureRepairsRestartedPeerAndWritesContinue() throws Excep
159159
final PartitionInfo partitionInfo = newPartitionInfo(locations);
160160
final ConfigManager configManager =
161161
Mockito.mock(ConfigManager.class, Mockito.RETURNS_DEEP_STUBS);
162-
Mockito.when(configManager.getPartitionManager().getAllReplicaSets())
163-
.thenAnswer(ignored -> partitionInfo.getAllReplicaSets());
162+
Mockito.when(configManager.getPartitionManager().getRegionDatabase(REGION))
163+
.thenAnswer(call -> partitionInfo.getRegionDatabase(call.getArgument(0)));
164+
Mockito.when(
165+
configManager
166+
.getPartitionManager()
167+
.getReplicaSets("root.restart_repair", Collections.singletonList(REGION)))
168+
.thenAnswer(
169+
call -> partitionInfo.getReplicaSets(call.getArgument(0), call.getArgument(1)));
164170
Mockito.when(
165171
configManager
166172
.getPartitionManager()

‎iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/region/RestartDuringRemovePeerTest.java‎

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import static org.apache.iotdb.consensus.ConsensusFactory.RATIS_CONSENSUS;
6767
import static org.junit.Assert.assertEquals;
6868
import static org.junit.Assert.assertFalse;
69+
import static org.junit.Assert.assertNull;
6970
import static org.junit.Assert.assertTrue;
7071
import static org.mockito.ArgumentMatchers.any;
7172
import static org.mockito.ArgumentMatchers.anyList;
@@ -117,8 +118,10 @@ public void setUp() throws Exception {
117118
loadManager = Mockito.mock(LoadManager.class, Mockito.RETURNS_DEEP_STUBS);
118119
when(configManager.getPartitionManager()).thenReturn(partitionManager);
119120
when(configManager.getLoadManager()).thenReturn(loadManager);
120-
when(partitionManager.getAllReplicaSets())
121-
.thenAnswer(ignored -> partitionInfo.getAllReplicaSets());
121+
when(partitionManager.getRegionDatabase(regionId))
122+
.thenAnswer(call -> partitionInfo.getRegionDatabase(call.getArgument(0)));
123+
when(partitionManager.getReplicaSets("root.restart", Collections.singletonList(regionId)))
124+
.thenAnswer(call -> partitionInfo.getReplicaSets(call.getArgument(0), call.getArgument(1)));
122125
when(partitionManager.removeRegionLocation(any()))
123126
.thenAnswer(call -> partitionInfo.removeRegionLocation(call.getArgument(0)));
124127
handler = Mockito.spy(new RegionMaintainHandler(configManager));
@@ -286,7 +289,7 @@ public void replayAfterSuccessfulCorrectionIsIdempotent() throws Exception {
286289
}
287290

288291
@Test
289-
public void offlineReplicaFailsAfterThreeRetriesInsteadOfWaitingForever() throws Exception {
292+
public void offlineReplicaCompletesRemovalAfterThreeRetries() throws Exception {
290293
AccessibleRemoveProcedure procedure = beforePeerListReset();
291294
doReturn(Collections.singletonMap(coordinator.getDataNodeId(), success()))
292295
.when(handler)
@@ -297,8 +300,27 @@ public void offlineReplicaFailsAfterThreeRetriesInsteadOfWaitingForever() throws
297300
assertFalse(procedure.isFailed());
298301
}
299302
assertEquals(0, procedure.doExecute(env).length);
300-
assertTrue(procedure.isFailed());
301-
assertTrue(procedure.getException().getMessage().contains("4"));
303+
assertFalse(procedure.isFailed());
304+
assertNull(procedure.getException());
305+
assertEquals(finalPeers, partitionInfo.getAllReplicaSets().get(0).getDataNodeLocations());
306+
verify(handler, times(4)).resetPeerList(eq(regionId), eq(finalPeers), anyMap());
307+
verify(handler).removeRegionLocation(regionId, removedNode);
308+
}
309+
310+
@Test
311+
public void repeatedCorrectionExceptionsAlsoCompleteRemovalAfterThreeRetries() throws Exception {
312+
AccessibleRemoveProcedure procedure = beforePeerListReset();
313+
doThrow(new IllegalStateException("injected RPC failure"))
314+
.when(handler)
315+
.resetPeerList(any(), anyList(), anyMap());
316+
for (int retry = 0; retry < 3; retry++) {
317+
assertEquals(1, procedure.doExecute(env).length);
318+
assertFalse(procedure.isFailed());
319+
}
320+
assertEquals(0, procedure.doExecute(env).length);
321+
assertFalse(procedure.isFailed());
322+
assertNull(procedure.getException());
323+
assertEquals(finalPeers, partitionInfo.getAllReplicaSets().get(0).getDataNodeLocations());
302324
verify(handler, times(4)).resetPeerList(eq(regionId), eq(finalPeers), anyMap());
303325
verify(handler).removeRegionLocation(regionId, removedNode);
304326
}
@@ -317,6 +339,24 @@ public void metadataFailureAlsoStopsAtRetryLimitWithoutResettingPeers() throws E
317339
verify(handler, never()).resetPeerList(any(), anyList(), anyMap());
318340
}
319341

342+
@Test
343+
public void metadataExceptionsStillFailAtRetryLimitWithoutResettingPeers() throws Exception {
344+
AccessibleRemoveProcedure procedure = beforeMetadataRemoval();
345+
IllegalStateException failure = new IllegalStateException("injected metadata failure");
346+
doThrow(failure).when(partitionManager).removeRegionLocation(any());
347+
for (int retry = 0; retry < 3; retry++) {
348+
assertEquals(1, procedure.doExecute(env).length);
349+
assertFalse(procedure.isFailed());
350+
}
351+
assertEquals(0, procedure.doExecute(env).length);
352+
assertTrue(procedure.isFailed());
353+
assertEquals(failure, procedure.getException().getCause());
354+
assertTrue(
355+
partitionInfo.getAllReplicaSets().get(0).getDataNodeLocations().contains(removedNode));
356+
verify(handler, times(4)).removeRegionLocation(regionId, removedNode);
357+
verify(handler, never()).resetPeerList(any(), anyList(), anyMap());
358+
}
359+
320360
@Test
321361
public void interruptedRetryRetainsItsStepForRecovery() throws Exception {
322362
AccessibleRemoveProcedure procedure = beforePeerListReset();
@@ -342,7 +382,7 @@ public void correctionRejectsStaleMetadataAndNullResponsesButAcceptsDeletedRegio
342382
partitionInfo.removeRegionLocation(new RemoveRegionLocationPlan(regionId, removedNode));
343383
doReturn(null).when(handler).resetPeerList(any(), anyList(), anyMap());
344384
assertFalse(handler.resetPeerListAfterRemoveForIoTConsensusV1(regionId, removedNode));
345-
when(partitionManager.getAllReplicaSets()).thenReturn(Collections.emptyList());
385+
when(partitionManager.getRegionDatabase(regionId)).thenReturn(null);
346386
assertTrue(handler.resetPeerListAfterRemoveForIoTConsensusV1(regionId, removedNode));
347387
}
348388

0 commit comments

Comments
 (0)