Skip to content

Commit eb5da01

Browse files
swuferhongcodex
andcommitted
[server] Make timed-out rebalance tasks recoverable
Keep timed-out bucket migrations under bounded reconciliation while admitting new work. Persist cancellation intent and final status, recover intermediate assignments, and require a current leader acknowledgement before completion. Fence stale responses and avoid repeated elections for applied leader moves. Expose recovery limits and replan failed migrations using the configured replica count. Document the state transitions and recovery settings, share test setup, and retain targeted failure and restart coverage. Exempt only ConfigOptions from the Checkstyle file length limit. Skip startup recovery on ZooKeeper read failure to allow new rebalance requests, and leave a TODO for recovery retries. Validation: 89 targeted and integration tests passed, including five rebalance integration cases. After the startup recovery adjustment, reran all 35 RebalanceManagerTest cases successfully. Checkstyle and Spotless checks passed. Co-Authored-By: Codex <noreply@openai.com> AI-Model: gpt-6 AI-Contributed/Feature: 7/7 AI-Contributed/UT: 0/0
1 parent cafca32 commit eb5da01

25 files changed

Lines changed: 3092 additions & 923 deletions

‎fluss-common/src/main/java/org/apache/fluss/cluster/rebalance/RebalanceStatus.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public enum RebalanceStatus {
3838
TIMEOUT(5);
3939

4040
public static final Set<RebalanceStatus> FINAL_STATUSES =
41-
new HashSet<>(Arrays.asList(COMPLETED, CANCELED, FAILED, TIMEOUT));
41+
new HashSet<>(Arrays.asList(COMPLETED, CANCELED, FAILED));
4242

4343
private final int code;
4444

‎fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,39 @@ public class ConfigOptions {
241241
+ "TableLifecycleThrottler scans in-flight drops for "
242242
+ "timeouts.");
243243

244+
public static final ConfigOption<Duration> COORDINATOR_REBALANCE_TARGET_UNAVAILABLE_TIMEOUT =
245+
key("coordinator.rebalance.target-unavailable-timeout")
246+
.durationType()
247+
.defaultValue(Duration.ofMinutes(30))
248+
.withDescription(
249+
"The time after which a timed-out bucket migration is marked FAILED "
250+
+ "if its target tablet servers remain unavailable and no "
251+
+ "bucket-state progress is observed. Observed progress or all "
252+
+ "target servers becoming live resets this timer. Must be at "
253+
+ "least 1 ms and fit in a signed 64-bit millisecond value.");
254+
255+
public static final ConfigOption<Duration> COORDINATOR_REBALANCE_NO_PROGRESS_TIMEOUT =
256+
key("coordinator.rebalance.no-progress-timeout")
257+
.durationType()
258+
.defaultValue(Duration.ofHours(24))
259+
.withDescription(
260+
"The time after which a timed-out bucket migration is marked FAILED "
261+
+ "if no bucket-state progress is observed, even when its target "
262+
+ "tablet servers are live. Observed progress resets this timer; "
263+
+ "this does not limit the total migration duration. Must be at "
264+
+ "least 1 ms and fit in a signed 64-bit millisecond value.");
265+
266+
public static final ConfigOption<Integer> COORDINATOR_REBALANCE_MAX_TRACKED_TIMED_OUT_TASKS =
267+
key("coordinator.rebalance.max-tracked-timed-out-tasks")
268+
.intType()
269+
.defaultValue(8)
270+
.withDescription(
271+
"The maximum number of non-final timed-out bucket migrations tracked "
272+
+ "before the coordinator stops admitting new bucket migrations. "
273+
+ "Admission resumes when a tracked migration reaches a final "
274+
+ "status. This limits concurrent migrations and reconciliation "
275+
+ "work on the coordinator and ZooKeeper. Must be at least 1.");
276+
244277
public static final ConfigOption<Duration> COORDINATOR_OFFLINE_LEADER_RETRY_DELAY =
245278
key("coordinator.offline-leader.retry-delay")
246279
.durationType()

‎fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessor.java‎

Lines changed: 329 additions & 107 deletions
Large diffs are not rendered by default.

‎fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorRequestBatch.java‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
import org.apache.fluss.server.coordinator.event.AccessContextEvent;
3939
import org.apache.fluss.server.coordinator.event.DeleteReplicaResponseReceivedEvent;
4040
import org.apache.fluss.server.coordinator.event.EventManager;
41+
import org.apache.fluss.server.coordinator.event.NotifyLeaderAndIsrRequestContext;
4142
import org.apache.fluss.server.coordinator.event.NotifyLeaderAndIsrResponseReceivedEvent;
43+
import org.apache.fluss.server.coordinator.rebalance.RebalanceExecutionKey;
4244
import org.apache.fluss.server.entity.DeleteReplicaResultForBucket;
4345
import org.apache.fluss.server.entity.NotifyLeaderAndIsrData;
4446
import org.apache.fluss.server.metadata.BucketMetadata;
@@ -60,6 +62,7 @@
6062
import java.util.Map;
6163
import java.util.Optional;
6264
import java.util.Set;
65+
import java.util.function.Function;
6366
import java.util.stream.Collectors;
6467

6568
import static org.apache.fluss.server.metadata.PartitionMetadata.DELETED_PARTITION_ID;
@@ -114,6 +117,8 @@ public class CoordinatorRequestBatch {
114117
private final CoordinatorChannelManager coordinatorChannelManager;
115118
private final EventManager eventManager;
116119
private final CoordinatorContext coordinatorContext;
120+
private Function<TableBucket, RebalanceExecutionKey> rebalanceExecutionKeyResolver =
121+
ignored -> null;
117122

118123
public CoordinatorRequestBatch(
119124
CoordinatorChannelManager coordinatorChannelManager,
@@ -124,6 +129,11 @@ public CoordinatorRequestBatch(
124129
this.coordinatorContext = coordinatorContext;
125130
}
126131

132+
void setRebalanceExecutionKeyResolver(
133+
Function<TableBucket, RebalanceExecutionKey> rebalanceExecutionKeyResolver) {
134+
this.rebalanceExecutionKeyResolver = rebalanceExecutionKeyResolver;
135+
}
136+
127137
public void newBatch() {
128138
if (!notifyLeaderAndIsrRequestMap.isEmpty()) {
129139
throw new IllegalStateException(
@@ -456,6 +466,19 @@ private void sendNotifyLeaderAndIsrRequest(int coordinatorEpoch) {
456466
NotifyLeaderAndIsrRequest notifyLeaderAndIsrRequest =
457467
makeNotifyLeaderAndIsrRequest(
458468
coordinatorEpoch, notifyRequestEntry.getValue().values());
469+
Map<TableBucket, NotifyLeaderAndIsrRequestContext> requestContexts = new HashMap<>();
470+
for (Map.Entry<TableBucket, PbNotifyLeaderAndIsrReqForBucket> entry :
471+
notifyRequestEntry.getValue().entrySet()) {
472+
PbNotifyLeaderAndIsrReqForBucket request = entry.getValue();
473+
requestContexts.put(
474+
entry.getKey(),
475+
new NotifyLeaderAndIsrRequestContext(
476+
coordinatorEpoch,
477+
request.getLeader(),
478+
request.getLeaderEpoch(),
479+
request.getBucketEpoch(),
480+
rebalanceExecutionKeyResolver.apply(entry.getKey())));
481+
}
459482

460483
// Track exactly which buckets THIS request marked as pending leader activation. Only
461484
// those entries (where leader == serverId) need to be cleared if the request fails
@@ -504,7 +527,9 @@ private void sendNotifyLeaderAndIsrRequest(int coordinatorEpoch) {
504527
// put the response receive event into the event manager
505528
eventManager.put(
506529
new NotifyLeaderAndIsrResponseReceivedEvent(
507-
getNotifyLeaderAndIsrResponseData(response), serverId));
530+
getNotifyLeaderAndIsrResponseData(response),
531+
serverId,
532+
requestContexts));
508533
});
509534
}
510535
notifyLeaderAndIsrRequestMap.clear();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.fluss.server.coordinator.event;
19+
20+
/** An event that retries persisting the final state of a rebalance. */
21+
public final class FinalizeRebalanceEvent implements CoordinatorEvent {
22+
private final String rebalanceId;
23+
24+
public FinalizeRebalanceEvent(String rebalanceId) {
25+
this.rebalanceId = rebalanceId;
26+
}
27+
28+
public String getRebalanceId() {
29+
return rebalanceId;
30+
}
31+
32+
@Override
33+
public String toString() {
34+
return "FinalizeRebalanceEvent{rebalanceId='" + rebalanceId + "'}";
35+
}
36+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.fluss.server.coordinator.event;
19+
20+
import org.apache.fluss.server.coordinator.rebalance.RebalanceExecutionKey;
21+
22+
import javax.annotation.Nullable;
23+
24+
/** The coordinator and bucket epochs of a sent NotifyLeaderAndIsr request. */
25+
public final class NotifyLeaderAndIsrRequestContext {
26+
27+
private final int coordinatorEpoch;
28+
private final int leader;
29+
private final int leaderEpoch;
30+
private final int bucketEpoch;
31+
private final @Nullable RebalanceExecutionKey rebalanceExecutionKey;
32+
33+
public NotifyLeaderAndIsrRequestContext(
34+
int coordinatorEpoch, int leader, int leaderEpoch, int bucketEpoch) {
35+
this(coordinatorEpoch, leader, leaderEpoch, bucketEpoch, null);
36+
}
37+
38+
public NotifyLeaderAndIsrRequestContext(
39+
int coordinatorEpoch,
40+
int leader,
41+
int leaderEpoch,
42+
int bucketEpoch,
43+
@Nullable RebalanceExecutionKey rebalanceExecutionKey) {
44+
this.coordinatorEpoch = coordinatorEpoch;
45+
this.leader = leader;
46+
this.leaderEpoch = leaderEpoch;
47+
this.bucketEpoch = bucketEpoch;
48+
this.rebalanceExecutionKey = rebalanceExecutionKey;
49+
}
50+
51+
public int getCoordinatorEpoch() {
52+
return coordinatorEpoch;
53+
}
54+
55+
public int getLeader() {
56+
return leader;
57+
}
58+
59+
public int getLeaderEpoch() {
60+
return leaderEpoch;
61+
}
62+
63+
public int getBucketEpoch() {
64+
return bucketEpoch;
65+
}
66+
67+
public @Nullable RebalanceExecutionKey getRebalanceExecutionKey() {
68+
return rebalanceExecutionKey;
69+
}
70+
71+
@Override
72+
public String toString() {
73+
return "NotifyLeaderAndIsrRequestContext{"
74+
+ "coordinatorEpoch="
75+
+ coordinatorEpoch
76+
+ ", leader="
77+
+ leader
78+
+ ", leaderEpoch="
79+
+ leaderEpoch
80+
+ ", bucketEpoch="
81+
+ bucketEpoch
82+
+ ", rebalanceExecutionKey="
83+
+ rebalanceExecutionKey
84+
+ '}';
85+
}
86+
}

‎fluss-server/src/main/java/org/apache/fluss/server/coordinator/event/NotifyLeaderAndIsrResponseReceivedEvent.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@
1717

1818
package org.apache.fluss.server.coordinator.event;
1919

20+
import org.apache.fluss.metadata.TableBucket;
2021
import org.apache.fluss.rpc.messages.NotifyLeaderAndIsrRequest;
2122
import org.apache.fluss.server.entity.NotifyLeaderAndIsrResultForBucket;
2223

24+
import java.util.Collections;
25+
import java.util.HashMap;
2326
import java.util.List;
27+
import java.util.Map;
2428

2529
/** An event for receive the response of {@link NotifyLeaderAndIsrRequest} from tablet server. */
2630
public class NotifyLeaderAndIsrResponseReceivedEvent implements CoordinatorEvent {
@@ -30,11 +34,21 @@ public class NotifyLeaderAndIsrResponseReceivedEvent implements CoordinatorEvent
3034
// the server id that return the response
3135
private final int responseServerId;
3236

37+
private final Map<TableBucket, NotifyLeaderAndIsrRequestContext> requestContexts;
38+
3339
public NotifyLeaderAndIsrResponseReceivedEvent(
3440
List<NotifyLeaderAndIsrResultForBucket> notifyLeaderAndIsrResultForBuckets,
3541
int responseServerId) {
42+
this(notifyLeaderAndIsrResultForBuckets, responseServerId, Collections.emptyMap());
43+
}
44+
45+
public NotifyLeaderAndIsrResponseReceivedEvent(
46+
List<NotifyLeaderAndIsrResultForBucket> notifyLeaderAndIsrResultForBuckets,
47+
int responseServerId,
48+
Map<TableBucket, NotifyLeaderAndIsrRequestContext> requestContexts) {
3649
this.notifyLeaderAndIsrResultForBuckets = notifyLeaderAndIsrResultForBuckets;
3750
this.responseServerId = responseServerId;
51+
this.requestContexts = Collections.unmodifiableMap(new HashMap<>(requestContexts));
3852
}
3953

4054
public int getResponseServerId() {
@@ -44,4 +58,8 @@ public int getResponseServerId() {
4458
public List<NotifyLeaderAndIsrResultForBucket> getNotifyLeaderAndIsrResultForBuckets() {
4559
return notifyLeaderAndIsrResultForBuckets;
4660
}
61+
62+
public NotifyLeaderAndIsrRequestContext getRequestContext(TableBucket tableBucket) {
63+
return requestContexts.get(tableBucket);
64+
}
4765
}

‎fluss-server/src/main/java/org/apache/fluss/server/coordinator/event/RebalanceTaskTimeoutEvent.java‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,23 @@
1717

1818
package org.apache.fluss.server.coordinator.event;
1919

20-
import org.apache.fluss.metadata.TableBucket;
20+
import org.apache.fluss.server.coordinator.rebalance.RebalanceExecutionKey;
2121

2222
/** An event fired when a rebalance task exceeds the timeout without completing. */
2323
public class RebalanceTaskTimeoutEvent implements CoordinatorEvent {
2424

25-
private final TableBucket tableBucket;
25+
private final RebalanceExecutionKey executionKey;
2626

27-
public RebalanceTaskTimeoutEvent(TableBucket tableBucket) {
28-
this.tableBucket = tableBucket;
27+
public RebalanceTaskTimeoutEvent(RebalanceExecutionKey executionKey) {
28+
this.executionKey = executionKey;
2929
}
3030

31-
public TableBucket getTableBucket() {
32-
return tableBucket;
31+
public RebalanceExecutionKey getExecutionKey() {
32+
return executionKey;
3333
}
3434

3535
@Override
3636
public String toString() {
37-
return "RebalanceTaskTimeoutEvent{tableBucket=" + tableBucket + "}";
37+
return "RebalanceTaskTimeoutEvent{executionKey=" + executionKey + "}";
3838
}
3939
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.fluss.server.coordinator.event;
19+
20+
import org.apache.fluss.server.coordinator.rebalance.RebalanceExecutionKey;
21+
22+
/** An event that reconciles a timed-out rebalance task against coordinator state. */
23+
public final class ReconcileRebalanceTaskEvent implements CoordinatorEvent {
24+
private final RebalanceExecutionKey executionKey;
25+
26+
public ReconcileRebalanceTaskEvent(RebalanceExecutionKey executionKey) {
27+
this.executionKey = executionKey;
28+
}
29+
30+
public RebalanceExecutionKey getExecutionKey() {
31+
return executionKey;
32+
}
33+
34+
@Override
35+
public String toString() {
36+
return "ReconcileRebalanceTaskEvent{executionKey=" + executionKey + "}";
37+
}
38+
}

‎fluss-server/src/main/java/org/apache/fluss/server/coordinator/event/RecoverRebalanceEvent.java‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@
1919

2020
import org.apache.fluss.server.zk.data.RebalanceTask;
2121

22-
/** Fired during startup to recover a rebalance task that was persisted in ZooKeeper. */
23-
public class RecoverRebalanceEvent implements CoordinatorEvent {
24-
22+
/** An event that recovers and reconciles a persisted rebalance task. */
23+
public final class RecoverRebalanceEvent implements CoordinatorEvent {
2524
private final RebalanceTask rebalanceTask;
2625

2726
public RecoverRebalanceEvent(RebalanceTask rebalanceTask) {

0 commit comments

Comments
 (0)