This is an automated email from the ASF dual-hosted git repository.
wuchong pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git
The following commit(s) were added to refs/heads/main by this push:
new 01c13e875 [kv] Support elect standby replicas for primary key table
(#2829)
01c13e875 is described below
commit 01c13e875b1730bf3825de5fbb3383f90fbbb0ef
Author: yunhong <[email protected]>
AuthorDate: Tue May 5 12:36:30 2026 +0800
[kv] Support elect standby replicas for primary key table (#2829)
---
fluss-rpc/src/main/proto/FlussApi.proto | 3 +
.../coordinator/CoordinatorEventProcessor.java | 1 +
.../statemachine/ReplicaLeaderElection.java | 201 +++++++++---
.../statemachine/ReplicaStateMachine.java | 3 +-
.../statemachine/TableBucketStateMachine.java | 60 +++-
.../server/entity/NotifyLeaderAndIsrData.java | 8 +
.../org/apache/fluss/server/replica/IsrState.java | 27 +-
.../org/apache/fluss/server/replica/Replica.java | 73 ++++-
.../fluss/server/utils/ServerRpcMessageUtils.java | 24 +-
.../apache/fluss/server/zk/data/LeaderAndIsr.java | 40 ++-
.../server/zk/data/LeaderAndIsrJsonSerde.java | 25 +-
.../coordinator/CoordinatorEventProcessorTest.java | 5 +-
.../CoordinatorHighAvailabilityITCase.java | 4 +-
.../server/coordinator/TestCoordinatorGateway.java | 1 +
.../statemachine/ReplicaLeaderElectionTest.java | 350 ++++++++++++++++++++-
.../statemachine/ReplicaStateMachineTest.java | 30 +-
.../statemachine/TableBucketStateMachineTest.java | 93 +++++-
.../fluss/server/log/remote/RemoteLogTestBase.java | 8 +-
.../metadata/ZkBasedMetadataProviderTest.java | 18 +-
.../fluss/server/replica/AdjustIsrITCase.java | 1 +
.../fluss/server/replica/AdjustIsrManagerTest.java | 36 ++-
.../apache/fluss/server/replica/AdjustIsrTest.java | 8 +-
.../replica/HighWatermarkPersistenceTest.java | 1 +
.../replica/KvRecoverFromRemoteLogITCase.java | 2 +
.../fluss/server/replica/ReplicaManagerTest.java | 4 +
.../apache/fluss/server/replica/ReplicaTest.java | 2 +
.../fluss/server/replica/ReplicaTestBase.java | 2 +
.../replica/fetcher/ReplicaFetcherITCase.java | 1 +
.../replica/fetcher/ReplicaFetcherManagerTest.java | 1 +
.../replica/fetcher/ReplicaFetcherThreadTest.java | 2 +
.../server/tablet/TabletServerShutdownITCase.java | 4 +-
.../fluss/server/tablet/TabletServiceITCase.java | 2 +
.../server/testutils/FlussClusterExtension.java | 5 +
.../fluss/server/zk/ZooKeeperClientTest.java | 31 +-
.../server/zk/data/LeaderAndIsrJsonSerdeTest.java | 38 ++-
35 files changed, 979 insertions(+), 135 deletions(-)
diff --git a/fluss-rpc/src/main/proto/FlussApi.proto
b/fluss-rpc/src/main/proto/FlussApi.proto
index b0f56cffb..e8381215f 100644
--- a/fluss-rpc/src/main/proto/FlussApi.proto
+++ b/fluss-rpc/src/main/proto/FlussApi.proto
@@ -959,6 +959,7 @@ message PbAdjustIsrReqForBucket {
repeated int32 new_isr = 4 [packed = true];
required int32 coordinator_epoch = 5;
required int32 bucket_epoch = 6;
+ repeated int32 standby_replicas = 7 [packed = true];
}
message PbAdjustIsrRespForTable {
@@ -976,6 +977,7 @@ message PbAdjustIsrRespForBucket {
repeated int32 isr = 7 [packed = true];
optional int32 bucket_epoch = 8;
optional int32 coordinator_epoch = 9;
+ repeated int32 standby_replicas = 10 [packed = true];
}
message PbListOffsetsRespForBucket {
@@ -996,6 +998,7 @@ message PbNotifyLeaderAndIsrReqForBucket {
repeated int32 replicas = 5 [packed = true];
repeated int32 isr = 6 [packed = true];
required int32 bucket_epoch = 7;
+ repeated int32 standby_replicas = 8 [packed = true];
}
message PbNotifyLeaderAndIsrRespForBucket {
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessor.java
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessor.java
index 89c294ea9..96f39ef08 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessor.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessor.java
@@ -1686,6 +1686,7 @@ public class CoordinatorEventProcessor implements
EventProcessor {
// TODO: reject the request if there is a replica
in ISR is not online,
// see KIP-841.
tryAdjustLeaderAndIsr.isr(),
+ tryAdjustLeaderAndIsr.standbyReplicas(),
coordinatorContext.getCoordinatorEpoch(),
currentLeaderAndIsr.bucketEpoch() + 1);
newLeaderAndIsrList.put(tableBucket, newLeaderAndIsr);
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/ReplicaLeaderElection.java
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/ReplicaLeaderElection.java
index e9b0d33cf..d63714423 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/ReplicaLeaderElection.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/ReplicaLeaderElection.java
@@ -21,6 +21,7 @@ import
org.apache.fluss.server.coordinator.statemachine.TableBucketStateMachine.
import org.apache.fluss.server.zk.data.LeaderAndIsr;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
@@ -41,23 +42,26 @@ public abstract class ReplicaLeaderElection {
* @param assignments the assignments
* @param aliveReplicas the alive replicas
* @param leaderAndIsr the original leaderAndIsr
+ * @param isPrimaryKeyTable whether this table bucket is primary key
table
* @return the election result
*/
public Optional<ElectionResult> leaderElection(
- List<Integer> assignments, List<Integer> aliveReplicas,
LeaderAndIsr leaderAndIsr) {
- // currently, we always use the first replica in assignment, which
also in aliveReplicas
- // and
- // isr as the leader replica.
+ List<Integer> assignments,
+ List<Integer> aliveReplicas,
+ LeaderAndIsr leaderAndIsr,
+ boolean isPrimaryKeyTable) {
List<Integer> isr = leaderAndIsr.isr();
- for (int assignment : assignments) {
- if (aliveReplicas.contains(assignment) &&
isr.contains(assignment)) {
- return Optional.of(
- new TableBucketStateMachine.ElectionResult(
- aliveReplicas,
leaderAndIsr.newLeaderAndIsr(assignment, isr)));
- }
- }
-
- return Optional.empty();
+ // First we will filter out the assignment list to only contain
the alive replicas and
+ // isr.
+ List<Integer> availableReplicas =
+ assignments.stream()
+ .filter(
+ replica ->
+ aliveReplicas.contains(replica)
+ && isr.contains(replica))
+ .collect(Collectors.toList());
+ return electLeader(
+ availableReplicas, aliveReplicas, isr, leaderAndIsr,
isPrimaryKeyTable);
}
}
@@ -70,32 +74,41 @@ public abstract class ReplicaLeaderElection {
* @param aliveReplicas the alive replicas
* @param leaderAndIsr the original leaderAndIsr
* @param shutdownTabletServers the shutdown tabletServers
+ * @param isPrimaryKeyTable whether this table bucket is primary key
table
* @return the election result
*/
public Optional<ElectionResult> leaderElection(
List<Integer> assignments,
List<Integer> aliveReplicas,
LeaderAndIsr leaderAndIsr,
- Set<Integer> shutdownTabletServers) {
+ Set<Integer> shutdownTabletServers,
+ boolean isPrimaryKeyTable) {
List<Integer> originIsr = leaderAndIsr.isr();
Set<Integer> isrSet = new HashSet<>(originIsr);
- for (Integer id : assignments) {
- if (aliveReplicas.contains(id)
- && isrSet.contains(id)
- && !shutdownTabletServers.contains(id)) {
- Set<Integer> newAliveReplicas = new
HashSet<>(aliveReplicas);
- newAliveReplicas.removeAll(shutdownTabletServers);
- List<Integer> newIsr =
- originIsr.stream()
- .filter(replica ->
!shutdownTabletServers.contains(replica))
- .collect(Collectors.toList());
- return Optional.of(
- new ElectionResult(
- new ArrayList<>(newAliveReplicas),
- leaderAndIsr.newLeaderAndIsr(id, newIsr)));
- }
- }
- return Optional.empty();
+ // Filter out available replicas: alive, in ISR, and not shutting
down.
+ List<Integer> availableReplicas =
+ assignments.stream()
+ .filter(
+ replica ->
+ aliveReplicas.contains(replica)
+ && isrSet.contains(replica)
+ &&
!shutdownTabletServers.contains(replica))
+ .collect(Collectors.toList());
+
+ // Filter alive replicas and ISR to exclude shutting-down servers.
+ Set<Integer> newAliveReplicaSet = new HashSet<>(aliveReplicas);
+ newAliveReplicaSet.removeAll(shutdownTabletServers);
+ List<Integer> newIsr =
+ originIsr.stream()
+ .filter(replica ->
!shutdownTabletServers.contains(replica))
+ .collect(Collectors.toList());
+
+ return electLeader(
+ availableReplicas,
+ new ArrayList<>(newAliveReplicaSet),
+ newIsr,
+ leaderAndIsr,
+ isPrimaryKeyTable);
}
}
@@ -108,20 +121,126 @@ public abstract class ReplicaLeaderElection {
}
public Optional<ElectionResult> leaderElection(
- List<Integer> liveReplicas, LeaderAndIsr leaderAndIsr) {
- // currently, we always use the first replica in targetReplicas,
which also in
- // liveReplicas and isr as the leader replica. For bucket
reassignment, the first
- // replica is the target leader replica.
+ List<Integer> liveReplicas, LeaderAndIsr leaderAndIsr, boolean
isPrimaryKeyTable) {
+ // For bucket reassignment, the first replica in newReplicas is
the target leader
+ // encoded by GoalOptimizerUtils. We must always honor this target
instead of using
+ // standby-promotion logic, otherwise the elected leader may
differ from the rebalance
+ // plan and tryToCompleteRebalanceTask will remain stuck in
REBALANCING state.
List<Integer> isr = leaderAndIsr.isr();
- for (int assignment : newReplicas) {
- if (liveReplicas.contains(assignment) &&
isr.contains(assignment)) {
- return Optional.of(
- new ElectionResult(
- liveReplicas,
leaderAndIsr.newLeaderAndIsr(assignment, isr)));
- }
+ List<Integer> availableReplicas =
+ newReplicas.stream()
+ .filter(
+ replica ->
+ liveReplicas.contains(replica) &&
isr.contains(replica))
+ .collect(Collectors.toList());
+
+ if (availableReplicas.isEmpty()) {
+ return Optional.empty();
}
+ // Always use the first available replica as leader to honor the
rebalance plan.
+ int newLeader = availableReplicas.get(0);
+ List<Integer> standbyReplicas =
+ isPrimaryKeyTable
+ ? findNewStandby(availableReplicas, newLeader)
+ : Collections.emptyList();
+
+ return Optional.of(
+ new ElectionResult(
+ liveReplicas,
+ leaderAndIsr.newLeaderAndIsr(newLeader, isr,
standbyReplicas)));
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Common election logic
+ // ------------------------------------------------------------------------
+
+ private static Optional<ElectionResult> electLeader(
+ List<Integer> availableReplicas,
+ List<Integer> liveReplicas,
+ List<Integer> newIsr,
+ LeaderAndIsr leaderAndIsr,
+ boolean isPrimaryKeyTable) {
+ if (availableReplicas.isEmpty()) {
return Optional.empty();
}
+
+ // For log table, simply use the first available replica as leader.
+ if (!isPrimaryKeyTable) {
+ return Optional.of(
+ new ElectionResult(
+ liveReplicas,
+ leaderAndIsr.newLeaderAndIsr(
+ availableReplicas.get(0), newIsr,
Collections.emptyList())));
+ }
+
+ // For PK table, elect leader and standby.
+ LeaderAndStandby leaderAndStandby =
+ electLeaderAndStandbyForPkTable(availableReplicas,
leaderAndIsr);
+ return Optional.of(
+ new ElectionResult(
+ liveReplicas,
+ leaderAndIsr.newLeaderAndIsr(
+ leaderAndStandby.leader,
+ newIsr,
+ leaderAndStandby.standbyReplicas)));
+ }
+
+ /**
+ * Elect leader and standby for PK table.
+ *
+ * <p>Election strategy:
+ *
+ * <ul>
+ * <li>If current standby exists and is available, promote it to leader
+ * <li>Otherwise, use the first available replica as leader
+ * <li>Select new standby from remaining available replicas (if any)
+ * </ul>
+ */
+ private static LeaderAndStandby electLeaderAndStandbyForPkTable(
+ List<Integer> availableReplicas, LeaderAndIsr leaderAndIsr) {
+ int currentStandby = getCurrentStandby(leaderAndIsr);
+ int newLeader;
+
+ if (currentStandby != -1 &&
availableReplicas.contains(currentStandby)) {
+ // Promote current standby to leader.
+ newLeader = currentStandby;
+ } else {
+ // Use first available replica as leader.
+ newLeader = availableReplicas.get(0);
+ }
+
+ // Find new standby from remaining replicas.
+ List<Integer> standbyReplicas = findNewStandby(availableReplicas,
newLeader);
+ return new LeaderAndStandby(newLeader, standbyReplicas);
+ }
+
+ /** Get current standby replica ID, returns -1 if no standby exists. */
+ private static int getCurrentStandby(LeaderAndIsr leaderAndIsr) {
+ return leaderAndIsr.standbyReplicas().isEmpty()
+ ? -1
+ : leaderAndIsr.standbyReplicas().get(0);
+ }
+
+ /** Find new standby from available replicas, excluding the leader. */
+ private static List<Integer> findNewStandby(
+ List<Integer> availableReplicas, int excludeLeader) {
+ return availableReplicas.stream()
+ .filter(replica -> replica != excludeLeader)
+ .findFirst()
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList());
+ }
+
+ /** Internal class to hold leader and standby election result. */
+ private static class LeaderAndStandby {
+ final int leader;
+ final List<Integer> standbyReplicas;
+
+ LeaderAndStandby(int leader, List<Integer> standbyReplicas) {
+ this.leader = leader;
+ this.standbyReplicas = standbyReplicas;
+ }
}
}
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/ReplicaStateMachine.java
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/ReplicaStateMachine.java
index 63f5dd965..d35bb7578 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/ReplicaStateMachine.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/ReplicaStateMachine.java
@@ -481,7 +481,8 @@ public class ReplicaStateMachine {
.collect(Collectors.toList());
LeaderAndIsr adjustLeaderAndIsr =
newLeader == LeaderAndIsr.NO_LEADER
- ? leaderAndIsr.newLeaderAndIsr(newLeader, newIsr)
+ ? leaderAndIsr.newLeaderAndIsr(
+ newLeader, newIsr,
leaderAndIsr.standbyReplicas())
: leaderAndIsr.newLeaderAndIsr(newIsr);
adjustedLeaderAndIsr.put(tableBucketReplica, adjustLeaderAndIsr);
toUpdateLeaderAndIsrList.put(tableBucket, adjustLeaderAndIsr);
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/TableBucketStateMachine.java
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/TableBucketStateMachine.java
index 300e98f08..6a556ddea 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/TableBucketStateMachine.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/statemachine/TableBucketStateMachine.java
@@ -440,7 +440,12 @@ public class TableBucketStateMachine {
// servers as inSyncReplica set.
Optional<ElectionResult> resultOpt =
initReplicaLeaderElection(
- assignedServers, liveServers,
coordinatorContext.getCoordinatorEpoch());
+ assignedServers,
+ liveServers,
+ coordinatorContext.getCoordinatorEpoch(),
+ coordinatorContext
+ .getTableInfoById(tableBucket.getTableId())
+ .hasPrimaryKey());
if (!resultOpt.isPresent()) {
LOG.error(
"The leader election for table bucket {} is empty.",
@@ -620,10 +625,12 @@ public class TableBucketStateMachine {
}
Optional<ElectionResult> resultOpt = Optional.empty();
+ boolean isPkTable =
+
coordinatorContext.getTableInfoById(tableBucket.getTableId()).hasPrimaryKey();
if (electionStrategy instanceof DefaultLeaderElection) {
resultOpt =
((DefaultLeaderElection) electionStrategy)
- .leaderElection(assignment, liveReplicas,
leaderAndIsr);
+ .leaderElection(assignment, liveReplicas,
leaderAndIsr, isPkTable);
} else if (electionStrategy instanceof
ControlledShutdownLeaderElection) {
Set<Integer> shuttingDownTabletServers =
coordinatorContext.shuttingDownTabletServers();
resultOpt =
@@ -632,11 +639,12 @@ public class TableBucketStateMachine {
assignment,
liveReplicas,
leaderAndIsr,
- shuttingDownTabletServers);
+ shuttingDownTabletServers,
+ isPkTable);
} else if (electionStrategy instanceof ReassignmentLeaderElection) {
resultOpt =
((ReassignmentLeaderElection) electionStrategy)
- .leaderElection(liveReplicas, leaderAndIsr);
+ .leaderElection(liveReplicas, leaderAndIsr,
isPkTable);
}
if (!resultOpt.isPresent()) {
@@ -677,23 +685,45 @@ public class TableBucketStateMachine {
* @param assignments the assignments
* @param aliveReplicas the alive replicas
* @param coordinatorEpoch the coordinator epoch
+ * @param isPrimaryKeyTable whether this table bucket is primary key table
* @return the election result
*/
@VisibleForTesting
public static Optional<ElectionResult> initReplicaLeaderElection(
- List<Integer> assignments, List<Integer> aliveReplicas, int
coordinatorEpoch) {
- // currently, we always use the first replica in assignment, which
also in aliveReplicas and
- // isr as the leader replica.
- for (int assignment : assignments) {
- if (aliveReplicas.contains(assignment)) {
- return Optional.of(
- new ElectionResult(
- aliveReplicas,
- new LeaderAndIsr(
- assignment, 0, aliveReplicas,
coordinatorEpoch, 0)));
+ List<Integer> assignments,
+ List<Integer> aliveReplicas,
+ int coordinatorEpoch,
+ boolean isPrimaryKeyTable) {
+ // First we will filter out the assignment list to only contain the
alive replicas.
+ List<Integer> availableReplicas =
+
assignments.stream().filter(aliveReplicas::contains).collect(Collectors.toList());
+
+ // If the assignment list is empty, we return empty.
+ if (availableReplicas.isEmpty()) {
+ return Optional.empty();
+ }
+
+ // Then we will use the first replica in assignment as the leader
replica.
+ int leader = availableReplicas.get(0);
+
+ // If this table is primaryKey table, we will use the second replica
in assignment as the
+ // standby if exists.
+ List<Integer> standbyReplicas = new ArrayList<>();
+ if (isPrimaryKeyTable) {
+ if (availableReplicas.size() > 1) {
+ standbyReplicas.add(availableReplicas.get(1));
}
}
- return Optional.empty();
+ return Optional.of(
+ new ElectionResult(
+ availableReplicas,
+ new LeaderAndIsr(
+ leader,
+ 0,
+ availableReplicas,
+ standbyReplicas,
+ coordinatorEpoch,
+ 0)));
}
}
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/entity/NotifyLeaderAndIsrData.java
b/fluss-server/src/main/java/org/apache/fluss/server/entity/NotifyLeaderAndIsrData.java
index 8ba000208..e6702cdca 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/entity/NotifyLeaderAndIsrData.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/entity/NotifyLeaderAndIsrData.java
@@ -85,4 +85,12 @@ public final class NotifyLeaderAndIsrData {
public LeaderAndIsr getLeaderAndIsr() {
return leaderAndIsr;
}
+
+ public List<Integer> getStandbyReplicas() {
+ return leaderAndIsr.standbyReplicas();
+ }
+
+ public int[] getStandbyReplicasArray() {
+ return
leaderAndIsr.standbyReplicas().stream().mapToInt(Integer::intValue).toArray();
+ }
}
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/replica/IsrState.java
b/fluss-server/src/main/java/org/apache/fluss/server/replica/IsrState.java
index aac5df385..b8b3b0a82 100644
--- a/fluss-server/src/main/java/org/apache/fluss/server/replica/IsrState.java
+++ b/fluss-server/src/main/java/org/apache/fluss/server/replica/IsrState.java
@@ -40,15 +40,25 @@ public interface IsrState {
*/
List<Integer> maximalIsr();
+ /**
+ * Returns the list of standby replicas for this bucket. Standby replicas
are follower replicas
+ * designated to maintain a recent KV snapshot so that they can be quickly
promoted to leader
+ * without a full data rebuild. Currently, at most one standby replica is
supported per bucket,
+ * and it is only applicable to primary-key tables.
+ */
+ List<Integer> standbyReplicas();
+
/** Indicates if we have an AdjustIsr request inflight. */
boolean isInflight();
/** Class to represent the committed isr state of a {@link TableBucket}. */
class CommittedIsrState implements IsrState {
private final List<Integer> isr;
+ private final List<Integer> standbyReplicas;
- public CommittedIsrState(List<Integer> isr) {
+ public CommittedIsrState(List<Integer> isr, List<Integer>
standbyReplicas) {
this.isr = isr;
+ this.standbyReplicas = standbyReplicas;
}
@Override
@@ -56,6 +66,11 @@ public interface IsrState {
return isr;
}
+ @Override
+ public List<Integer> standbyReplicas() {
+ return standbyReplicas;
+ }
+
@Override
public List<Integer> maximalIsr() {
return isr;
@@ -135,6 +150,11 @@ public interface IsrState {
return lastCommittedState.isr();
}
+ @Override
+ public List<Integer> standbyReplicas() {
+ return lastCommittedState.standbyReplicas();
+ }
+
@Override
public List<Integer> maximalIsr() {
ArrayList<Integer> maximalIsr = new
ArrayList<>(lastCommittedState.isr());
@@ -205,6 +225,11 @@ public interface IsrState {
return lastCommittedState.isr();
}
+ @Override
+ public List<Integer> standbyReplicas() {
+ return lastCommittedState.standbyReplicas();
+ }
+
@Override
public List<Integer> maximalIsr() {
return lastCommittedState.isr();
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/replica/Replica.java
b/fluss-server/src/main/java/org/apache/fluss/server/replica/Replica.java
index 7a4881323..2259ec4c3 100644
--- a/fluss-server/src/main/java/org/apache/fluss/server/replica/Replica.java
+++ b/fluss-server/src/main/java/org/apache/fluss/server/replica/Replica.java
@@ -199,10 +199,12 @@ public final class Replica {
*/
private final Map<Integer, FollowerReplica> followerReplicasMap = new
ConcurrentHashMap<>();
- private volatile IsrState isrState = new
IsrState.CommittedIsrState(Collections.emptyList());
+ private volatile IsrState isrState =
+ new IsrState.CommittedIsrState(Collections.emptyList(),
Collections.emptyList());
private volatile int leaderEpoch = LeaderAndIsr.INITIAL_LEADER_EPOCH - 1;
private volatile int bucketEpoch = LeaderAndIsr.INITIAL_BUCKET_EPOCH;
private volatile int coordinatorEpoch =
CoordinatorContext.INITIAL_COORDINATOR_EPOCH;
+ private volatile boolean isStandbyReplica = false;
// null if table without pk or haven't become leader
private volatile @Nullable KvTablet kvTablet;
@@ -413,7 +415,11 @@ public final class Replica {
long currentTimeMs = clock.milliseconds();
// Updating the assignment and ISR state is safe
if the bucket epoch is
// larger or equal to the current bucket epoch.
- updateAssignmentAndIsr(data.getReplicas(), true,
data.getIsr());
+ updateAssignmentAndIsr(
+ data.getReplicas(),
+ true,
+ data.getIsr(),
+ data.getStandbyReplicas());
int requestLeaderEpoch = data.getLeaderEpoch();
if (requestLeaderEpoch > leaderEpoch) {
@@ -464,7 +470,11 @@ public final class Replica {
coordinatorEpoch = data.getCoordinatorEpoch();
- updateAssignmentAndIsr(Collections.emptyList(), false,
Collections.emptyList());
+ updateAssignmentAndIsr(
+ Collections.emptyList(),
+ false,
+ Collections.emptyList(),
+ Collections.emptyList());
int requestLeaderEpoch = data.getLeaderEpoch();
boolean isNewLeaderEpoch = requestLeaderEpoch >
leaderEpoch;
@@ -474,7 +484,10 @@ public final class Replica {
tableBucket,
requestLeaderEpoch,
logTablet.localLogEndOffset());
- onBecomeNewFollower();
+ // Currently, we only support one standby replica.
+ List<Integer> standbyReplicas =
data.getStandbyReplicas();
+ onBecomeNewFollower(
+ standbyReplicas.isEmpty() ? -1 :
standbyReplicas.get(0));
} else if (requestLeaderEpoch == leaderEpoch) {
LOG.info(
"Skipped the become-follower state change for
bucket {} since "
@@ -534,6 +547,9 @@ public final class Replica {
//
-------------------------------------------------------------------------------------------
private void onBecomeNewLeader() {
+ // Clear standby flag — a leader is never a standby replica.
+ isStandbyReplica = false;
+
updateLeaderEndOffsetSnapshot();
if (isDataLakeEnabled()) {
@@ -575,11 +591,28 @@ public final class Replica {
: logTablet.localMaxTimestamp() -
logTablet.getLakeMaxTimestamp());
}
- private void onBecomeNewFollower() {
+ private void onBecomeNewFollower(int standbyReplica) {
if (isKvTable()) {
+ boolean isNowStandby = (standbyReplica == localTabletServerId);
+ boolean wasLeader = isLeader();
+ boolean wasStandby = this.isStandbyReplica;
+ if (isNowStandby) {
+ // Mark as standby immediately to ensure coordinator's state
is consistent.
+ isStandbyReplica = true;
+ } else {
+ if (wasStandby || wasLeader) {
+ // standby -> follower or leader -> follower
+ if (wasStandby) {
+ isStandbyReplica = false;
+ }
+ }
+ // follower -> follower: do nothing.
+ }
+
// it should be from leader to follower, we need to destroy the kv
tablet
dropKv();
}
+
if (lakeTieringMetricGroup != null) {
lakeTieringMetricGroup.close();
}
@@ -1165,7 +1198,10 @@ public final class Replica {
}
private void updateAssignmentAndIsr(
- List<Integer> replicas, boolean isLeader, List<Integer> isr) {
+ List<Integer> replicas,
+ boolean isLeader,
+ List<Integer> isr,
+ List<Integer> standbyReplicas) {
if (isLeader) {
List<Integer> followers =
replicas.stream()
@@ -1188,7 +1224,7 @@ public final class Replica {
}
// update isr info.
- isrState = new IsrState.CommittedIsrState(isr);
+ isrState = new IsrState.CommittedIsrState(isr, standbyReplicas);
}
private void updateFollowerFetchState(
@@ -1719,9 +1755,19 @@ public final class Replica {
// TODO add server epoch to isr.
+ // TODO: For PK tables, if the current standby list is empty (e.g.,
the previous standby
+ // went down), we should consider assigning a standby from the
expanded ISR members.
+ // This also requires sending a NotifyLeaderAndIsr to the newly
assigned standby replica
+ // so it can start maintaining KV snapshots. Without this, buckets
that lose their standby
+ // will not regain one until the next leader election.
LeaderAndIsr newLeaderAndIsr =
new LeaderAndIsr(
- localTabletServerId, leaderEpoch, isrToSend,
coordinatorEpoch, bucketEpoch);
+ localTabletServerId,
+ leaderEpoch,
+ isrToSend,
+ currentState.standbyReplicas(),
+ coordinatorEpoch,
+ bucketEpoch);
IsrState.PendingExpandIsrState updatedState =
new IsrState.PendingExpandIsrState(
@@ -1743,7 +1789,12 @@ public final class Replica {
LeaderAndIsr newLeaderAndIsr =
new LeaderAndIsr(
- localTabletServerId, leaderEpoch, isrToSend,
coordinatorEpoch, bucketEpoch);
+ localTabletServerId,
+ leaderEpoch,
+ isrToSend,
+ currentState.standbyReplicas(),
+ coordinatorEpoch,
+ bucketEpoch);
IsrState.PendingShrinkIsrState updatedState =
new IsrState.PendingShrinkIsrState(
outOfSyncFollowerReplicas, newLeaderAndIsr,
currentState);
@@ -1838,7 +1889,9 @@ public final class Replica {
// proposed and actual state are the same.
// In both cases, we want to move from Pending to Committed state
to ensure new updates
// are processed.
- isrState = new IsrState.CommittedIsrState(leaderAndIsr.isr());
+ isrState =
+ new IsrState.CommittedIsrState(
+ leaderAndIsr.isr(),
leaderAndIsr.standbyReplicas());
bucketEpoch = leaderAndIsr.bucketEpoch();
LOG.info(
"ISR updated to {} and bucket epoch updated to {} for
bucket {}",
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java
b/fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java
index d37c36148..f7a89828a 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java
@@ -684,7 +684,8 @@ public class ServerRpcMessageUtils {
new PbNotifyLeaderAndIsrReqForBucket()
.setLeader(notifyLeaderAndIsrData.getLeader())
.setLeaderEpoch(notifyLeaderAndIsrData.getLeaderEpoch())
-
.setBucketEpoch(notifyLeaderAndIsrData.getBucketEpoch());
+
.setBucketEpoch(notifyLeaderAndIsrData.getBucketEpoch())
+
.setStandbyReplicas(notifyLeaderAndIsrData.getStandbyReplicasArray());
TableBucket tb = notifyLeaderAndIsrData.getTableBucket();
PbTableBucket pbTableBucket =
@@ -720,6 +721,11 @@ public class ServerRpcMessageUtils {
isr.add(reqForBucket.getIsrAt(i));
}
+ List<Integer> standbyReplicas = new ArrayList<>();
+ for (int i = 0; i < reqForBucket.getStandbyReplicasCount(); i++) {
+ standbyReplicas.add(reqForBucket.getStandbyReplicaAt(i));
+ }
+
PbTableBucket pbTableBucket = reqForBucket.getTableBucket();
notifyLeaderAndIsrDataList.add(
new NotifyLeaderAndIsrData(
@@ -730,6 +736,7 @@ public class ServerRpcMessageUtils {
reqForBucket.getLeader(),
reqForBucket.getLeaderEpoch(),
isr,
+ standbyReplicas,
request.getCoordinatorEpoch(),
reqForBucket.getBucketEpoch())));
}
@@ -1295,6 +1302,7 @@ public class ServerRpcMessageUtils {
reqForBucket.setPartitionId(tb.getPartitionId());
}
leaderAndIsr.isr().forEach(reqForBucket::addNewIsr);
+
leaderAndIsr.standbyReplicas().forEach(reqForBucket::addStandbyReplica);
if (reqForBucketByTableId.containsKey(tb.getTableId())) {
reqForBucketByTableId.get(tb.getTableId()).add(reqForBucket);
} else {
@@ -1336,12 +1344,18 @@ public class ServerRpcMessageUtils {
for (int i = 0; i < reqForBucket.getNewIsrsCount(); i++) {
newIsr.add(reqForBucket.getNewIsrAt(i));
}
+ List<Integer> standbyReplicas = new ArrayList<>();
+ for (int i = 0; i < reqForBucket.getStandbyReplicasCount();
i++) {
+ standbyReplicas.add(reqForBucket.getStandbyReplicaAt(i));
+ }
+
leaderAndIsrMap.put(
tb,
new LeaderAndIsr(
leaderId,
reqForBucket.getLeaderEpoch(),
newIsr,
+ standbyReplicas,
reqForBucket.getCoordinatorEpoch(),
reqForBucket.getBucketEpoch()));
}
@@ -1368,7 +1382,8 @@ public class ServerRpcMessageUtils {
.setLeaderEpoch(leaderAndIsr.leaderEpoch())
.setCoordinatorEpoch(leaderAndIsr.coordinatorEpoch())
.setBucketEpoch(leaderAndIsr.bucketEpoch())
- .setIsrs(leaderAndIsr.isrArray());
+ .setIsrs(leaderAndIsr.isrArray())
+
.setStandbyReplicas(leaderAndIsr.standbyReplicasArray());
}
if (respMap.containsKey(tb.getTableId())) {
@@ -1417,6 +1432,10 @@ public class ServerRpcMessageUtils {
for (int i = 0; i < respForBucket.getIsrsCount(); i++) {
isr.add(respForBucket.getIsrAt(i));
}
+ List<Integer> standbyReplicas = new ArrayList<>();
+ for (int i = 0; i < respForBucket.getStandbyReplicasCount();
i++) {
+ standbyReplicas.add(respForBucket.getStandbyReplicaAt(i));
+ }
adjustIsrResult.put(
tb,
new AdjustIsrResultForBucket(
@@ -1425,6 +1444,7 @@ public class ServerRpcMessageUtils {
respForBucket.getLeaderId(),
respForBucket.getLeaderEpoch(),
isr,
+ standbyReplicas,
respForBucket.getCoordinatorEpoch(),
respForBucket.getBucketEpoch())));
}
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/zk/data/LeaderAndIsr.java
b/fluss-server/src/main/java/org/apache/fluss/server/zk/data/LeaderAndIsr.java
index 251248138..020601716 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/zk/data/LeaderAndIsr.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/zk/data/LeaderAndIsr.java
@@ -43,6 +43,8 @@ public class LeaderAndIsr {
/** The latest inSyncReplica collection. */
private final List<Integer> isr;
+ private final List<Integer> standbyReplicas;
+
/** The coordinator epoch. */
private final int coordinatorEpoch;
@@ -57,15 +59,22 @@ public class LeaderAndIsr {
leader,
INITIAL_LEADER_EPOCH,
new ArrayList<>(),
+ new ArrayList<>(),
coordinatorEpoch,
INITIAL_BUCKET_EPOCH);
}
public LeaderAndIsr(
- int leader, int leaderEpoch, List<Integer> isr, int
coordinatorEpoch, int bucketEpoch) {
+ int leader,
+ int leaderEpoch,
+ List<Integer> isr,
+ List<Integer> standbyReplicas,
+ int coordinatorEpoch,
+ int bucketEpoch) {
this.leader = leader;
this.leaderEpoch = leaderEpoch;
this.isr = checkNotNull(isr);
+ this.standbyReplicas = checkNotNull(standbyReplicas);
this.coordinatorEpoch = coordinatorEpoch;
this.bucketEpoch = bucketEpoch;
}
@@ -77,9 +86,15 @@ public class LeaderAndIsr {
* @param newIsr the new isr
* @return the new LeaderAndIsr
*/
- public LeaderAndIsr newLeaderAndIsr(int newLeader, List<Integer> newIsr) {
+ public LeaderAndIsr newLeaderAndIsr(
+ int newLeader, List<Integer> newIsr, List<Integer>
standbyReplicas) {
return new LeaderAndIsr(
- newLeader, leaderEpoch + 1, newIsr, coordinatorEpoch,
bucketEpoch + 1);
+ newLeader,
+ leaderEpoch + 1,
+ newIsr,
+ standbyReplicas,
+ coordinatorEpoch,
+ bucketEpoch + 1);
}
/**
@@ -90,7 +105,8 @@ public class LeaderAndIsr {
* @return the new LeaderAndIsr
*/
public LeaderAndIsr newLeaderAndIsr(List<Integer> newIsr) {
- return new LeaderAndIsr(leader, leaderEpoch, newIsr, coordinatorEpoch,
bucketEpoch + 1);
+ return new LeaderAndIsr(
+ leader, leaderEpoch, newIsr, standbyReplicas,
coordinatorEpoch, bucketEpoch + 1);
}
public int leader() {
@@ -109,10 +125,18 @@ public class LeaderAndIsr {
return isr;
}
+ public List<Integer> standbyReplicas() {
+ return standbyReplicas;
+ }
+
public int[] isrArray() {
return isr.stream().mapToInt(Integer::intValue).toArray();
}
+ public int[] standbyReplicasArray() {
+ return standbyReplicas.stream().mapToInt(Integer::intValue).toArray();
+ }
+
public int bucketEpoch() {
return bucketEpoch;
}
@@ -130,12 +154,14 @@ public class LeaderAndIsr {
&& leaderEpoch == that.leaderEpoch
&& coordinatorEpoch == that.coordinatorEpoch
&& bucketEpoch == that.bucketEpoch
- && Objects.equals(isr, that.isr);
+ && Objects.equals(isr, that.isr)
+ && Objects.equals(standbyReplicas, that.standbyReplicas);
}
@Override
public int hashCode() {
- return Objects.hash(leader, leaderEpoch, isr, coordinatorEpoch,
bucketEpoch);
+ return Objects.hash(
+ leader, leaderEpoch, isr, standbyReplicas, coordinatorEpoch,
bucketEpoch);
}
@Override
@@ -147,6 +173,8 @@ public class LeaderAndIsr {
+ leaderEpoch
+ ", isr="
+ isr
+ + ", standbyReplicas="
+ + standbyReplicas
+ ", coordinatorEpoch="
+ coordinatorEpoch
+ ", bucketEpoch="
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/zk/data/LeaderAndIsrJsonSerde.java
b/fluss-server/src/main/java/org/apache/fluss/server/zk/data/LeaderAndIsrJsonSerde.java
index 9d76330f2..65c69d1c4 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/zk/data/LeaderAndIsrJsonSerde.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/zk/data/LeaderAndIsrJsonSerde.java
@@ -35,18 +35,19 @@ public class LeaderAndIsrJsonSerde
public static final LeaderAndIsrJsonSerde INSTANCE = new
LeaderAndIsrJsonSerde();
private static final String VERSION_KEY = "version";
- private static final int VERSION = 1;
+ private static final int CURRENT_VERSION = 2;
private static final String LEADER = "leader";
private static final String LEADER_EPOCH = "leader_epoch";
private static final String ISR = "isr";
private static final String COORDINATOR_EPOCH = "coordinator_epoch";
private static final String BUCKET_EPOCH = "bucket_epoch";
+ private static final String STANDBY_REPLICA = "standby_replicas";
@Override
public void serialize(LeaderAndIsr leaderAndIsr, JsonGenerator generator)
throws IOException {
generator.writeStartObject();
- generator.writeNumberField(VERSION_KEY, VERSION);
+ generator.writeNumberField(VERSION_KEY, CURRENT_VERSION);
generator.writeNumberField(LEADER, leaderAndIsr.leader());
generator.writeNumberField(LEADER_EPOCH, leaderAndIsr.leaderEpoch());
generator.writeArrayFieldStart(ISR);
@@ -57,11 +58,19 @@ public class LeaderAndIsrJsonSerde
generator.writeNumberField(COORDINATOR_EPOCH,
leaderAndIsr.coordinatorEpoch());
generator.writeNumberField(BUCKET_EPOCH, leaderAndIsr.bucketEpoch());
+ // write standby_replicas
+ generator.writeArrayFieldStart(STANDBY_REPLICA);
+ for (Integer replica : leaderAndIsr.standbyReplicas()) {
+ generator.writeNumber(replica);
+ }
+ generator.writeEndArray();
+
generator.writeEndObject();
}
@Override
public LeaderAndIsr deserialize(JsonNode node) {
+ int version = node.get(VERSION_KEY).asInt();
int leader = node.get(LEADER).asInt();
int leaderEpoch = node.get(LEADER_EPOCH).asInt();
int coordinatorEpoch = node.get(COORDINATOR_EPOCH).asInt();
@@ -71,6 +80,16 @@ public class LeaderAndIsrJsonSerde
while (isrNodes.hasNext()) {
isr.add(isrNodes.next().asInt());
}
- return new LeaderAndIsr(leader, leaderEpoch, isr, coordinatorEpoch,
bucketEpoch);
+
+ List<Integer> standbyList = new ArrayList<>();
+ if (version >= CURRENT_VERSION) {
+ Iterator<JsonNode> hotStandbyListNodes =
node.get(STANDBY_REPLICA).elements();
+ while (hotStandbyListNodes.hasNext()) {
+ standbyList.add(hotStandbyListNodes.next().asInt());
+ }
+ }
+
+ return new LeaderAndIsr(
+ leader, leaderEpoch, isr, standbyList, coordinatorEpoch,
bucketEpoch);
}
}
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessorTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessorTest.java
index e4f60a938..0694e9787 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessorTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorEventProcessorTest.java
@@ -777,6 +777,7 @@ class CoordinatorEventProcessorTest {
leader,
leaderAndIsr.leaderEpoch(),
newIsr,
+ Collections.emptyList(),
coordinatorEpoch,
bucketLeaderEpoch));
return null;
@@ -1035,7 +1036,9 @@ class CoordinatorEventProcessorTest {
CompletableFuture<AdjustIsrResponse> respCallback = new
CompletableFuture<>();
// This isr list equals originReplicas + addingReplicas. the bucket
epoch is 1.
- leaderAndIsrMap.put(tb0, new LeaderAndIsr(0, 0, Arrays.asList(0, 1, 2,
3), 0, 1));
+ leaderAndIsrMap.put(
+ tb0,
+ new LeaderAndIsr(0, 0, Arrays.asList(0, 1, 2, 3),
Collections.emptyList(), 0, 1));
eventProcessor
.getCoordinatorEventManager()
.put(new AdjustIsrReceivedEvent(leaderAndIsrMap,
respCallback));
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorHighAvailabilityITCase.java
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorHighAvailabilityITCase.java
index 68e46e359..23b8a70bf 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorHighAvailabilityITCase.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorHighAvailabilityITCase.java
@@ -56,6 +56,7 @@ import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletionException;
@@ -332,7 +333,8 @@ class CoordinatorHighAvailabilityITCase {
.isEqualTo(standby.getServerId());
TableBucket tableBucket = new TableBucket(1, 1);
- LeaderAndIsr leaderAndIsr = new LeaderAndIsr(0, 0, Arrays.asList(2,
3), 0, 0);
+ LeaderAndIsr leaderAndIsr =
+ new LeaderAndIsr(0, 0, Arrays.asList(2, 3),
Collections.emptyList(), 0, 0);
int newLeaderEpochZkVersion =
standby.getCoordinatorEventProcessor()
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/TestCoordinatorGateway.java
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/TestCoordinatorGateway.java
index f55846a7f..98e295f9a 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/TestCoordinatorGateway.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/TestCoordinatorGateway.java
@@ -324,6 +324,7 @@ public class TestCoordinatorGateway implements
CoordinatorGateway {
leaderAndIsr.leader(),
currentLeaderEpoch,
leaderAndIsr.isr(),
+ leaderAndIsr.standbyReplicas(),
leaderAndIsr.coordinatorEpoch(),
leaderAndIsr.bucketEpoch() +
1));
}
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/ReplicaLeaderElectionTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/ReplicaLeaderElectionTest.java
index d1dc77d61..313114d69 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/ReplicaLeaderElectionTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/ReplicaLeaderElectionTest.java
@@ -41,30 +41,90 @@ public class ReplicaLeaderElectionTest {
void testDefaultReplicaLeaderElection() {
List<Integer> assignments = Arrays.asList(2, 4);
List<Integer> liveReplicas = Arrays.asList(2, 4);
- LeaderAndIsr originLeaderAndIsr = new LeaderAndIsr(4, 0,
Arrays.asList(2, 4), 0, 0);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(4, 0, Arrays.asList(2, 4),
Collections.emptyList(), 0, 0);
DefaultLeaderElection defaultLeaderElection = new
DefaultLeaderElection();
Optional<ElectionResult> leaderElectionResultOpt =
- defaultLeaderElection.leaderElection(assignments,
liveReplicas, originLeaderAndIsr);
+ defaultLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr, false);
assertThat(leaderElectionResultOpt.isPresent()).isTrue();
ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(2,
4);
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(2);
assertThat(leaderElectionResult.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(2,
4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas()).isEmpty();
+ }
+
+ @Test
+ void testDefaultReplicaLeaderElectionForPkTable() {
+ List<Integer> assignments = Arrays.asList(2, 3, 4);
+ List<Integer> liveReplicas = Arrays.asList(3, 4);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(2, 0, Arrays.asList(2, 3, 4),
Collections.emptyList(), 0, 0);
+
+ // first, test origin leaderAndIsr don't have standby replica.
+ DefaultLeaderElection defaultLeaderElection = new
DefaultLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ defaultLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
+
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(3,
4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(3);
+
assertThat(leaderElectionResult.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(2,
3, 4);
+ assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas())
+ .containsExactlyInAnyOrder(4);
+
+ // second. test origin leaderAndIsr has standby replica.
+ originLeaderAndIsr =
+ new LeaderAndIsr(2, 0, Arrays.asList(2, 3, 4),
Collections.singletonList(4), 0, 0);
+ defaultLeaderElection = new DefaultLeaderElection();
+ leaderElectionResultOpt =
+ defaultLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ leaderElectionResult = leaderElectionResultOpt.get();
+
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(3,
4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(2,
3, 4);
+ assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas())
+ .containsExactlyInAnyOrder(3);
+
+ // third. test no enough live replicas.
+ assignments = Arrays.asList(2, 3, 4);
+ liveReplicas = Collections.singletonList(4);
+ originLeaderAndIsr =
+ new LeaderAndIsr(2, 0, Arrays.asList(2, 3, 4),
Collections.emptyList(), 0, 0);
+ defaultLeaderElection = new DefaultLeaderElection();
+ leaderElectionResultOpt =
+ defaultLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ leaderElectionResult = leaderElectionResultOpt.get();
+
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(2,
3, 4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas()).isEmpty();
}
@Test
void testControlledShutdownReplicaLeaderElection() {
List<Integer> assignments = Arrays.asList(2, 4);
List<Integer> liveReplicas = Arrays.asList(2, 4);
- LeaderAndIsr originLeaderAndIsr = new LeaderAndIsr(2, 0,
Arrays.asList(2, 4), 0, 0);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(2, 0, Arrays.asList(2, 4),
Collections.emptyList(), 0, 0);
Set<Integer> shutdownTabletServers = Collections.singleton(2);
ControlledShutdownLeaderElection controlledShutdownLeaderElection =
new ControlledShutdownLeaderElection();
Optional<ElectionResult> leaderElectionResultOpt =
controlledShutdownLeaderElection.leaderElection(
- assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers);
+ assignments,
+ liveReplicas,
+ originLeaderAndIsr,
+ shutdownTabletServers,
+ false);
assertThat(leaderElectionResultOpt.isPresent()).isTrue();
ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(4);
@@ -77,57 +137,317 @@ public class ReplicaLeaderElectionTest {
List<Integer> assignments = Arrays.asList(2, 4);
List<Integer> liveReplicas = Arrays.asList(2, 4);
LeaderAndIsr originLeaderAndIsr =
- new LeaderAndIsr(2, 0, Collections.singletonList(2), 0, 0);
+ new LeaderAndIsr(2, 0, Collections.singletonList(2),
Collections.emptyList(), 0, 0);
Set<Integer> shutdownTabletServers = Collections.singleton(2);
ControlledShutdownLeaderElection controlledShutdownLeaderElection =
new ControlledShutdownLeaderElection();
Optional<ElectionResult> leaderElectionResultOpt =
controlledShutdownLeaderElection.leaderElection(
- assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers);
+ assignments,
+ liveReplicas,
+ originLeaderAndIsr,
+ shutdownTabletServers,
+ false);
assertThat(leaderElectionResultOpt).isEmpty();
}
@Test
- void
testControlledShutdownPartitionLeaderElectionAllIsrSimultaneouslyShutdown() {
+ void testControlledShutdownLeaderElectionAllIsrSimultaneouslyShutdown() {
List<Integer> assignments = Arrays.asList(2, 4);
List<Integer> liveReplicas = Arrays.asList(2, 4);
- LeaderAndIsr originLeaderAndIsr = new LeaderAndIsr(2, 0,
Arrays.asList(2, 4), 0, 0);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(2, 0, Arrays.asList(2, 4),
Collections.emptyList(), 0, 0);
Set<Integer> shutdownTabletServers = new HashSet<>(Arrays.asList(2,
4));
ControlledShutdownLeaderElection controlledShutdownLeaderElection =
new ControlledShutdownLeaderElection();
Optional<ElectionResult> leaderElectionResultOpt =
controlledShutdownLeaderElection.leaderElection(
- assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers);
+ assignments,
+ liveReplicas,
+ originLeaderAndIsr,
+ shutdownTabletServers,
+ false);
assertThat(leaderElectionResultOpt).isEmpty();
}
+ @Test
+ void testControlledShutdownReplicaLeaderElectionForPkTable() {
+ List<Integer> assignments = Arrays.asList(2, 3, 4);
+ List<Integer> liveReplicas = Arrays.asList(2, 3, 4);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(2, 0, Arrays.asList(2, 3, 4),
Collections.emptyList(), 0, 0);
+ Set<Integer> shutdownTabletServers = Collections.singleton(2);
+ ControlledShutdownLeaderElection controlledShutdownLeaderElection =
+ new ControlledShutdownLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ controlledShutdownLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
+
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(3,
4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(3);
+
assertThat(leaderElectionResult.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(3,
4);
+ assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas())
+ .containsExactlyInAnyOrder(4);
+
+ originLeaderAndIsr =
+ new LeaderAndIsr(2, 0, Arrays.asList(2, 3, 4),
Collections.singletonList(4), 0, 0);
+ controlledShutdownLeaderElection = new
ControlledShutdownLeaderElection();
+ leaderElectionResultOpt =
+ controlledShutdownLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ leaderElectionResult = leaderElectionResultOpt.get();
+
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(3,
4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(3,
4);
+ assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas())
+ .containsExactlyInAnyOrder(3);
+ }
+
@Test
void testReassignBucketLeaderElection() {
List<Integer> targetReplicas = Arrays.asList(1, 2, 3);
ReassignmentLeaderElection reassignmentLeaderElection =
new ReassignmentLeaderElection(targetReplicas);
List<Integer> liveReplicas = Arrays.asList(1, 2, 3);
- LeaderAndIsr leaderAndIsr = new LeaderAndIsr(1, 0, Arrays.asList(1, 2,
3), 0, 0);
+ LeaderAndIsr leaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(1, 2, 3),
Collections.emptyList(), 0, 0);
Optional<ElectionResult> leaderOpt =
- reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr);
+ reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr, false);
assertThat(leaderOpt).isPresent();
assertThat(leaderOpt.get().getLeaderAndIsr().leader()).isEqualTo(1);
targetReplicas = Arrays.asList(1, 2, 3);
reassignmentLeaderElection = new
ReassignmentLeaderElection(targetReplicas);
liveReplicas = Arrays.asList(2, 3);
- leaderAndIsr = new LeaderAndIsr(1, 0, Arrays.asList(2, 3), 0, 0);
- leaderOpt = reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr);
+ leaderAndIsr = new LeaderAndIsr(1, 0, Arrays.asList(2, 3),
Collections.emptyList(), 0, 0);
+ leaderOpt = reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr, false);
assertThat(leaderOpt).isPresent();
assertThat(leaderOpt.get().getLeaderAndIsr().leader()).isEqualTo(2);
targetReplicas = Arrays.asList(1, 2, 3);
reassignmentLeaderElection = new
ReassignmentLeaderElection(targetReplicas);
liveReplicas = Arrays.asList(1, 2);
- leaderAndIsr = new LeaderAndIsr(2, 1, Collections.emptyList(), 0, 1);
- leaderOpt = reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr);
+ leaderAndIsr =
+ new LeaderAndIsr(2, 1, Collections.emptyList(),
Collections.emptyList(), 0, 1);
+ leaderOpt = reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr, false);
assertThat(leaderOpt).isNotPresent();
}
+
+ @Test
+ void testDefaultLeaderElectionWithSingleReplica() {
+ // Test that single replica election doesn't throw
IndexOutOfBoundsException
+ List<Integer> assignments = Collections.singletonList(1);
+ List<Integer> liveReplicas = Collections.singletonList(1);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(1, 0, Collections.singletonList(1),
Collections.emptyList(), 0, 0);
+
+ DefaultLeaderElection defaultLeaderElection = new
DefaultLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ defaultLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(1);
+ // With only one replica, standby should be empty
+
assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas()).isEmpty();
+ }
+
+ @Test
+ void testControlledShutdownWithSingleRemainingReplica() {
+ // Test controlled shutdown when only one replica remains available
+ List<Integer> assignments = Arrays.asList(1, 2);
+ List<Integer> liveReplicas = Arrays.asList(1, 2);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(1, 2),
Collections.singletonList(2), 0, 0);
+ Set<Integer> shutdownTabletServers = Collections.singleton(1);
+
+ ControlledShutdownLeaderElection controlledShutdownLeaderElection =
+ new ControlledShutdownLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ controlledShutdownLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
+ // Standby (2) should be promoted to leader
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(2);
+ // With only one remaining replica, standby should be empty
+
assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas()).isEmpty();
+ }
+
+ @Test
+ void testReassignBucketLeaderElectionForPkTable() {
+ // Test reassignment election for PK table
+ List<Integer> targetReplicas = Arrays.asList(1, 2, 3);
+ ReassignmentLeaderElection reassignmentLeaderElection =
+ new ReassignmentLeaderElection(targetReplicas);
+ List<Integer> liveReplicas = Arrays.asList(1, 2, 3);
+ LeaderAndIsr leaderAndIsr =
+ new LeaderAndIsr(2, 0, Arrays.asList(1, 2, 3),
Collections.singletonList(3), 0, 0);
+ Optional<ElectionResult> leaderOpt =
+ reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr, true);
+ assertThat(leaderOpt).isPresent();
+ // First available replica (1) should be leader to honor the rebalance
plan
+ assertThat(leaderOpt.get().getLeaderAndIsr().leader()).isEqualTo(1);
+ // A new standby should be assigned from remaining available replicas
+ assertThat(leaderOpt.get().getLeaderAndIsr().standbyReplicas())
+ .containsExactlyInAnyOrder(2);
+ }
+
+ @Test
+ void testReassignBucketLeaderElectionForPkTableWithSingleReplica() {
+ // Test reassignment election for PK table with only one replica
+ List<Integer> targetReplicas = Collections.singletonList(1);
+ ReassignmentLeaderElection reassignmentLeaderElection =
+ new ReassignmentLeaderElection(targetReplicas);
+ List<Integer> liveReplicas = Collections.singletonList(1);
+ LeaderAndIsr leaderAndIsr =
+ new LeaderAndIsr(1, 0, Collections.singletonList(1),
Collections.emptyList(), 0, 0);
+ Optional<ElectionResult> leaderOpt =
+ reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr, true);
+ assertThat(leaderOpt).isPresent();
+ assertThat(leaderOpt.get().getLeaderAndIsr().leader()).isEqualTo(1);
+ // With only one replica, standby should be empty
+
assertThat(leaderOpt.get().getLeaderAndIsr().standbyReplicas()).isEmpty();
+ }
+
+ @Test
+ void testDefaultLeaderElectionWithStandbyUnavailable() {
+ // Test when current standby is not in available replicas
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Arrays.asList(1, 2); // standby (3) is
not alive
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(1, 2, 3),
Collections.singletonList(3), 0, 0);
+
+ DefaultLeaderElection defaultLeaderElection = new
DefaultLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ defaultLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
+ // Should select first available replica as leader since standby is
unavailable
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(1);
+ // New standby should be selected from remaining available replicas
+ assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas())
+ .containsExactlyInAnyOrder(2);
+ }
+
+ @Test
+ void testDefaultLeaderElectionWithNoAvailableReplicas() {
+ // Test when no replicas are available
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Collections.emptyList();
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(1, 2, 3),
Collections.emptyList(), 0, 0);
+
+ DefaultLeaderElection defaultLeaderElection = new
DefaultLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ defaultLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr, true);
+ assertThat(leaderElectionResultOpt).isEmpty();
+ }
+
+ @Test
+ void testControlledShutdownForPkTableWithStandbyShuttingDown() {
+ // Test when the current standby is shutting down
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Arrays.asList(1, 2, 3);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(1, 2, 3),
Collections.singletonList(2), 0, 0);
+ Set<Integer> shutdownTabletServers = Collections.singleton(2); //
standby is shutting down
+
+ ControlledShutdownLeaderElection controlledShutdownLeaderElection =
+ new ControlledShutdownLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ controlledShutdownLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
+ // Leader (1) should remain leader since standby (2) is shutting down
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(1);
+ // New standby should be selected from remaining replicas
+ assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas())
+ .containsExactlyInAnyOrder(3);
+ }
+
+ @Test
+ void
testControlledShutdownForPkTableWithLeaderAndStandbyBothShuttingDown() {
+ // Test when both leader and standby are shutting down simultaneously
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Arrays.asList(1, 2, 3);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(1, 2, 3),
Collections.singletonList(2), 0, 0);
+ // Both leader (1) and standby (2) are shutting down
+ Set<Integer> shutdownTabletServers = new HashSet<>(Arrays.asList(1,
2));
+
+ ControlledShutdownLeaderElection controlledShutdownLeaderElection =
+ new ControlledShutdownLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ controlledShutdownLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers, true);
+ assertThat(leaderElectionResultOpt.isPresent()).isTrue();
+ ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
+ // Replica 3 should become leader since both leader and standby are
shutting down
+
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(3);
+
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(3);
+
assertThat(leaderElectionResult.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(3);
+ // No remaining replicas for standby
+
assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas()).isEmpty();
+ }
+
+ @Test
+ void testControlledShutdownAllIsrShutdownForPkTable() {
+ // Test when all ISR replicas are shutting down for PK table
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Arrays.asList(1, 2, 3);
+ LeaderAndIsr originLeaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(1, 2, 3),
Collections.singletonList(2), 0, 0);
+ Set<Integer> shutdownTabletServers = new HashSet<>(Arrays.asList(1, 2,
3));
+
+ ControlledShutdownLeaderElection controlledShutdownLeaderElection =
+ new ControlledShutdownLeaderElection();
+ Optional<ElectionResult> leaderElectionResultOpt =
+ controlledShutdownLeaderElection.leaderElection(
+ assignments, liveReplicas, originLeaderAndIsr,
shutdownTabletServers, true);
+ // No available replicas, should return empty
+ assertThat(leaderElectionResultOpt).isEmpty();
+ }
+
+ @Test
+ void testReassignBucketLeaderElectionForPkTableWithStandbyUnavailable() {
+ // Test reassignment election for PK table when current standby is not
in new replicas
+ List<Integer> targetReplicas = Arrays.asList(4, 5, 6);
+ ReassignmentLeaderElection reassignmentLeaderElection =
+ new ReassignmentLeaderElection(targetReplicas);
+ List<Integer> liveReplicas = Arrays.asList(4, 5, 6);
+ // Current standby (3) is not in the new target replicas
+ LeaderAndIsr leaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(4, 5, 6),
Collections.singletonList(3), 0, 0);
+ Optional<ElectionResult> leaderOpt =
+ reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr, true);
+ assertThat(leaderOpt).isPresent();
+ // First available replica should be leader since old standby (3) is
not available
+ assertThat(leaderOpt.get().getLeaderAndIsr().leader()).isEqualTo(4);
+ // New standby should be selected from remaining available replicas
+ assertThat(leaderOpt.get().getLeaderAndIsr().standbyReplicas())
+ .containsExactlyInAnyOrder(5);
+ }
+
+ @Test
+ void testReassignBucketLeaderElectionForPkTableNoAvailableReplicas() {
+ // Test reassignment election for PK table when no replicas are
available
+ List<Integer> targetReplicas = Arrays.asList(1, 2, 3);
+ ReassignmentLeaderElection reassignmentLeaderElection =
+ new ReassignmentLeaderElection(targetReplicas);
+ List<Integer> liveReplicas = Arrays.asList(4, 5); // None of the
targets are live
+ LeaderAndIsr leaderAndIsr =
+ new LeaderAndIsr(1, 0, Arrays.asList(4, 5),
Collections.singletonList(2), 0, 0);
+ Optional<ElectionResult> leaderOpt =
+ reassignmentLeaderElection.leaderElection(liveReplicas,
leaderAndIsr, true);
+ // No available replicas should return empty
+ assertThat(leaderOpt).isEmpty();
+ }
}
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/ReplicaStateMachineTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/ReplicaStateMachineTest.java
index f1ad2fe1c..4d1946ab4 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/ReplicaStateMachineTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/ReplicaStateMachineTest.java
@@ -219,7 +219,8 @@ class ReplicaStateMachineTest {
replicas.add(replica);
}
// put leader and isr
- LeaderAndIsr leaderAndIsr = new LeaderAndIsr(0, 0, Arrays.asList(0, 1,
2), 0, 0);
+ LeaderAndIsr leaderAndIsr =
+ new LeaderAndIsr(0, 0, Arrays.asList(0, 1, 2),
Collections.emptyList(), 0, 0);
zookeeperClient.registerLeaderAndIsr(
tableBucket, leaderAndIsr,
ZkVersion.MATCH_ANY_VERSION.getVersion());
coordinatorContext.updateBucketReplicaAssignment(tableBucket,
Arrays.asList(0, 1, 2));
@@ -230,7 +231,14 @@ class ReplicaStateMachineTest {
replicaStateMachine.handleStateChanges(replicas, OfflineReplica);
leaderAndIsr =
coordinatorContext.getBucketLeaderAndIsr(tableBucket).get();
assertThat(leaderAndIsr)
- .isEqualTo(new LeaderAndIsr(LeaderAndIsr.NO_LEADER, 3,
Arrays.asList(2), 0, 3));
+ .isEqualTo(
+ new LeaderAndIsr(
+ LeaderAndIsr.NO_LEADER,
+ 3,
+ Collections.singletonList(2),
+ Collections.emptyList(),
+ 0,
+ 3));
}
@Test
@@ -257,7 +265,8 @@ class ReplicaStateMachineTest {
coordinatorContext.putReplicaState(replica, OnlineReplica);
}
// put leader and isr
- LeaderAndIsr leaderAndIsr = new LeaderAndIsr(0, 0, Arrays.asList(0, 1,
2), 0, 0);
+ LeaderAndIsr leaderAndIsr =
+ new LeaderAndIsr(0, 0, Arrays.asList(0, 1, 2),
Collections.emptyList(), 0, 0);
zookeeperClient.registerLeaderAndIsr(
tableBucket, leaderAndIsr,
ZkVersion.MATCH_ANY_VERSION.getVersion());
coordinatorContext.updateBucketReplicaAssignment(tableBucket,
Arrays.asList(0, 1, 2));
@@ -267,14 +276,18 @@ class ReplicaStateMachineTest {
replicaStateMachine.handleStateChanges(
Collections.singleton(new TableBucketReplica(tableBucket, 1)),
OfflineReplica);
leaderAndIsr =
coordinatorContext.getBucketLeaderAndIsr(tableBucket).get();
- assertThat(leaderAndIsr).isEqualTo(new LeaderAndIsr(0, 0,
Arrays.asList(0, 2), 0, 1));
+ assertThat(leaderAndIsr)
+ .isEqualTo(
+ new LeaderAndIsr(0, 0, Arrays.asList(0, 2),
Collections.emptyList(), 0, 1));
// set replica 2 to offline
replicaStateMachine.handleStateChanges(
Collections.singleton(new TableBucketReplica(tableBucket, 2)),
OfflineReplica);
leaderAndIsr =
coordinatorContext.getBucketLeaderAndIsr(tableBucket).get();
assertThat(leaderAndIsr)
- .isEqualTo(new LeaderAndIsr(0, 0,
Collections.singletonList(0), 0, 2));
+ .isEqualTo(
+ new LeaderAndIsr(
+ 0, 0, Collections.singletonList(0),
Collections.emptyList(), 0, 2));
// set replica 0 to offline, isr shouldn't be empty, leader should be
NO_LEADER
replicaStateMachine.handleStateChanges(
@@ -283,7 +296,12 @@ class ReplicaStateMachineTest {
assertThat(leaderAndIsr)
.isEqualTo(
new LeaderAndIsr(
- LeaderAndIsr.NO_LEADER, 1,
Collections.singletonList(0), 0, 3));
+ LeaderAndIsr.NO_LEADER,
+ 1,
+ Collections.singletonList(0),
+ Collections.emptyList(),
+ 0,
+ 3));
}
private void toReplicaDeletionStartedState(
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/TableBucketStateMachineTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/TableBucketStateMachineTest.java
index 2ea3718ff..7d80fe41c 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/TableBucketStateMachineTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/statemachine/TableBucketStateMachineTest.java
@@ -150,6 +150,24 @@ class TableBucketStateMachineTest {
TableBucket t2b0 = new TableBucket(t2Id, 0);
coordinatorContext.putTablePath(t1Id, TablePath.of("db1", "t1"));
coordinatorContext.putTablePath(t2Id, TablePath.of("db1", "t2"));
+ coordinatorContext.putTableInfo(
+ TableInfo.of(
+ TablePath.of("db1", "t1"),
+ t1Id,
+ 0,
+ DATA1_TABLE_DESCRIPTOR,
+ DEFAULT_REMOTE_DATA_DIR,
+ System.currentTimeMillis(),
+ System.currentTimeMillis()));
+ coordinatorContext.putTableInfo(
+ TableInfo.of(
+ TablePath.of("db1", "t2"),
+ t2Id,
+ 0,
+ DATA1_TABLE_DESCRIPTOR,
+ DEFAULT_REMOTE_DATA_DIR,
+ System.currentTimeMillis(),
+ System.currentTimeMillis()));
coordinatorContext.setLiveTabletServers(createServers(Arrays.asList(0,
1, 3)));
makeSendLeaderAndStopRequestAlwaysSuccess(
@@ -162,11 +180,11 @@ class TableBucketStateMachineTest {
// create LeaderAndIsr for t10/t11 info in zk,
zookeeperClient.registerLeaderAndIsr(
new TableBucket(t1Id, 0),
- new LeaderAndIsr(0, 0, Arrays.asList(0, 1), 0, 0),
+ new LeaderAndIsr(0, 0, Arrays.asList(0, 1),
Collections.emptyList(), 0, 0),
ZkVersion.MATCH_ANY_VERSION.getVersion());
zookeeperClient.registerLeaderAndIsr(
new TableBucket(t1Id, 1),
- new LeaderAndIsr(2, 0, Arrays.asList(2, 3), 0, 0),
+ new LeaderAndIsr(2, 0, Arrays.asList(2, 3),
Collections.emptyList(), 0, 0),
ZkVersion.MATCH_ANY_VERSION.getVersion());
// update the LeaderAndIsr to context
coordinatorContext.putBucketLeaderAndIsr(
@@ -311,6 +329,16 @@ class TableBucketStateMachineTest {
tableId = 5;
final TableBucket tableBucket1 = new TableBucket(tableId, 0);
coordinatorContext.putTablePath(tableId, fakeTablePath);
+ coordinatorContext.putTableInfo(
+ TableInfo.of(
+ fakeTablePath,
+ tableId,
+ 0,
+ DATA1_TABLE_DESCRIPTOR,
+ DEFAULT_REMOTE_DATA_DIR,
+ System.currentTimeMillis(),
+ System.currentTimeMillis()));
+ coordinatorContext.putTablePath(tableId, fakeTablePath);
coordinatorContext.updateBucketReplicaAssignment(tableBucket1,
Arrays.asList(0, 1, 2));
coordinatorContext.putBucketState(tableBucket1, NewBucket);
tableBucketStateMachine.handleStateChange(
@@ -418,12 +446,71 @@ class TableBucketStateMachineTest {
List<Integer> liveReplicas = Collections.singletonList(4);
Optional<ElectionResult> leaderElectionResultOpt =
- initReplicaLeaderElection(assignments, liveReplicas, 0);
+ initReplicaLeaderElection(assignments, liveReplicas, 0, false);
assertThat(leaderElectionResultOpt.isPresent()).isTrue();
ElectionResult leaderElectionResult = leaderElectionResultOpt.get();
assertThat(leaderElectionResult.getLiveReplicas()).containsExactlyInAnyOrder(4);
assertThat(leaderElectionResult.getLeaderAndIsr().leader()).isEqualTo(4);
assertThat(leaderElectionResult.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(4);
+
assertThat(leaderElectionResult.getLeaderAndIsr().standbyReplicas()).isEmpty();
+ }
+
+ @Test
+ void testInitReplicaLeaderElectionForPkTable() {
+ // PK table with multiple available replicas: first as leader, second
as standby
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Arrays.asList(1, 2, 3);
+
+ Optional<ElectionResult> resultOpt =
+ initReplicaLeaderElection(assignments, liveReplicas, 0, true);
+ assertThat(resultOpt).isPresent();
+ ElectionResult result = resultOpt.get();
+ assertThat(result.getLeaderAndIsr().leader()).isEqualTo(1);
+
assertThat(result.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(1, 2, 3);
+
assertThat(result.getLeaderAndIsr().standbyReplicas()).containsExactly(2);
+ }
+
+ @Test
+ void testInitReplicaLeaderElectionForPkTableWithSingleAvailableReplica() {
+ // PK table with only one available replica: leader only, no standby
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Collections.singletonList(2);
+
+ Optional<ElectionResult> resultOpt =
+ initReplicaLeaderElection(assignments, liveReplicas, 0, true);
+ assertThat(resultOpt).isPresent();
+ ElectionResult result = resultOpt.get();
+ assertThat(result.getLeaderAndIsr().leader()).isEqualTo(2);
+
assertThat(result.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(2);
+ assertThat(result.getLeaderAndIsr().standbyReplicas()).isEmpty();
+ }
+
+ @Test
+ void testInitReplicaLeaderElectionForPkTableWithNoAliveReplica() {
+ // PK table with no alive replicas: should return empty
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Collections.emptyList();
+
+ Optional<ElectionResult> resultOpt =
+ initReplicaLeaderElection(assignments, liveReplicas, 0, true);
+ assertThat(resultOpt).isEmpty();
+ }
+
+ @Test
+ void testInitReplicaLeaderElectionForPkTableWithPartiallyAliveReplicas() {
+ // PK table where first assigned replica is not alive,
+ // second and third are alive
+ List<Integer> assignments = Arrays.asList(1, 2, 3);
+ List<Integer> liveReplicas = Arrays.asList(2, 3);
+
+ Optional<ElectionResult> resultOpt =
+ initReplicaLeaderElection(assignments, liveReplicas, 0, true);
+ assertThat(resultOpt).isPresent();
+ ElectionResult result = resultOpt.get();
+ // First available (2) should be leader, second available (3) should
be standby
+ assertThat(result.getLeaderAndIsr().leader()).isEqualTo(2);
+
assertThat(result.getLeaderAndIsr().isr()).containsExactlyInAnyOrder(2, 3);
+
assertThat(result.getLeaderAndIsr().standbyReplicas()).containsExactly(3);
}
private TableBucketStateMachine createTableBucketStateMachine() {
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/log/remote/RemoteLogTestBase.java
b/fluss-server/src/test/java/org/apache/fluss/server/log/remote/RemoteLogTestBase.java
index 13ab2f7ba..ef6b2875d 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/log/remote/RemoteLogTestBase.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/log/remote/RemoteLogTestBase.java
@@ -87,7 +87,13 @@ public class RemoteLogTestBase extends ReplicaTestBase {
physicalTablePath,
tb,
Collections.singletonList(0),
- new LeaderAndIsr(0, 0, Collections.singletonList(0),
0, 0)));
+ new LeaderAndIsr(
+ 0,
+ 0,
+ Collections.singletonList(0),
+ Collections.emptyList(),
+ 0,
+ 0)));
addMultiSegmentsToLogTablet(replica.getLogTablet(), segmentSize);
return replica;
}
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/metadata/ZkBasedMetadataProviderTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/metadata/ZkBasedMetadataProviderTest.java
index 5597bb01b..6ef53b62a 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/metadata/ZkBasedMetadataProviderTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/metadata/ZkBasedMetadataProviderTest.java
@@ -108,8 +108,10 @@ class ZkBasedMetadataProviderTest {
TableBucket tableBucket0 = new TableBucket(tableId, 0);
TableBucket tableBucket1 = new TableBucket(tableId, 1);
- LeaderAndIsr leaderAndIsr0 = new LeaderAndIsr(1, 10, Arrays.asList(1,
2, 3), 100, 1000);
- LeaderAndIsr leaderAndIsr1 = new LeaderAndIsr(2, 20, Arrays.asList(2,
3, 4), 200, 2000);
+ LeaderAndIsr leaderAndIsr0 =
+ new LeaderAndIsr(1, 10, Arrays.asList(1, 2, 3),
Collections.emptyList(), 100, 1000);
+ LeaderAndIsr leaderAndIsr1 =
+ new LeaderAndIsr(2, 20, Arrays.asList(2, 3, 4),
Collections.emptyList(), 200, 2000);
zookeeperClient.registerLeaderAndIsr(
tableBucket0, leaderAndIsr0,
ZkVersion.MATCH_ANY_VERSION.getVersion());
@@ -176,8 +178,10 @@ class ZkBasedMetadataProviderTest {
TableBucket partitionBucket0 = new TableBucket(tableId, partitionId,
0);
TableBucket partitionBucket1 = new TableBucket(tableId, partitionId,
1);
- LeaderAndIsr leaderAndIsr0 = new LeaderAndIsr(1, 10, Arrays.asList(1,
2), 100, 1000);
- LeaderAndIsr leaderAndIsr1 = new LeaderAndIsr(2, 20, Arrays.asList(2,
3), 200, 2000);
+ LeaderAndIsr leaderAndIsr0 =
+ new LeaderAndIsr(1, 10, Arrays.asList(1, 2),
Collections.emptyList(), 100, 1000);
+ LeaderAndIsr leaderAndIsr1 =
+ new LeaderAndIsr(2, 20, Arrays.asList(2, 3),
Collections.emptyList(), 200, 2000);
zookeeperClient.registerLeaderAndIsr(
partitionBucket0, leaderAndIsr0,
ZkVersion.MATCH_ANY_VERSION.getVersion());
@@ -281,15 +285,15 @@ class ZkBasedMetadataProviderTest {
zookeeperClient.registerLeaderAndIsr(
bucket1,
- new LeaderAndIsr(1, 10, Arrays.asList(1, 2), 100, 1000),
+ new LeaderAndIsr(1, 10, Arrays.asList(1, 2),
Collections.emptyList(), 100, 1000),
ZkVersion.MATCH_ANY_VERSION.getVersion());
zookeeperClient.registerLeaderAndIsr(
bucket2,
- new LeaderAndIsr(2, 20, Arrays.asList(2, 3), 200, 2000),
+ new LeaderAndIsr(2, 20, Arrays.asList(2, 3),
Collections.emptyList(), 200, 2000),
ZkVersion.MATCH_ANY_VERSION.getVersion());
zookeeperClient.registerLeaderAndIsr(
bucket3,
- new LeaderAndIsr(1, 30, Arrays.asList(1, 3), 300, 3000),
+ new LeaderAndIsr(1, 30, Arrays.asList(1, 3),
Collections.emptyList(), 300, 3000),
ZkVersion.MATCH_ANY_VERSION.getVersion());
// Test getPartitionsMetadataFromZK
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrITCase.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrITCase.java
index 2931ca546..846a1f5f5 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrITCase.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrITCase.java
@@ -135,6 +135,7 @@ public class AdjustIsrITCase {
currentLeaderAndIsr.leader(),
currentLeaderAndIsr.leaderEpoch() + 1,
isr,
+ currentLeaderAndIsr.standbyReplicas(),
currentLeaderAndIsr.coordinatorEpoch(),
currentLeaderAndIsr.bucketEpoch());
isr.add(stopFollower);
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrManagerTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrManagerTest.java
index 248e7929e..7bdffcafe 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrManagerTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrManagerTest.java
@@ -26,6 +26,7 @@ import org.apache.fluss.utils.concurrent.FlussScheduler;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,17 +47,32 @@ class AdjustIsrManagerTest {
// shrink isr
TableBucket tb = new TableBucket(150001L, 0);
List<Integer> currentIsr = Arrays.asList(1, 2);
- LeaderAndIsr adjustIsr = new LeaderAndIsr(tabletServerId, 0,
currentIsr, 0, 0);
+ LeaderAndIsr adjustIsr =
+ new LeaderAndIsr(tabletServerId, 0, currentIsr,
Collections.emptyList(), 0, 0);
LeaderAndIsr result = adjustIsrManager.submit(tb, adjustIsr).get();
assertThat(result)
- .isEqualTo(new LeaderAndIsr(tabletServerId, 0,
Arrays.asList(1, 2), 0, 1));
+ .isEqualTo(
+ new LeaderAndIsr(
+ tabletServerId,
+ 0,
+ Arrays.asList(1, 2),
+ Collections.emptyList(),
+ 0,
+ 1));
// expand isr
currentIsr = Arrays.asList(1, 2, 3);
- adjustIsr = new LeaderAndIsr(tabletServerId, 0, currentIsr, 0, 1);
+ adjustIsr = new LeaderAndIsr(tabletServerId, 0, currentIsr,
Collections.emptyList(), 0, 1);
result = adjustIsrManager.submit(tb, adjustIsr).get();
assertThat(result)
- .isEqualTo(new LeaderAndIsr(tabletServerId, 0,
Arrays.asList(1, 2, 3), 0, 2));
+ .isEqualTo(
+ new LeaderAndIsr(
+ tabletServerId,
+ 0,
+ Arrays.asList(1, 2, 3),
+ Collections.emptyList(),
+ 0,
+ 2));
}
@Test
@@ -71,7 +87,8 @@ class AdjustIsrManagerTest {
// The RPC-level exception is propagated to the submit future.
TableBucket tb = new TableBucket(150001L, 0);
List<Integer> currentIsr = Arrays.asList(1, 2);
- LeaderAndIsr adjustIsr = new LeaderAndIsr(tabletServerId, 0,
currentIsr, 0, 0);
+ LeaderAndIsr adjustIsr =
+ new LeaderAndIsr(tabletServerId, 0, currentIsr,
Collections.emptyList(), 0, 0);
assertThatThrownBy(() -> adjustIsrManager.submit(tb, adjustIsr).get())
.rootCause()
.isInstanceOf(NetworkException.class)
@@ -83,6 +100,13 @@ class AdjustIsrManagerTest {
// submit.
LeaderAndIsr result = adjustIsrManager.submit(tb, adjustIsr).get();
assertThat(result)
- .isEqualTo(new LeaderAndIsr(tabletServerId, 0,
Arrays.asList(1, 2), 0, 1));
+ .isEqualTo(
+ new LeaderAndIsr(
+ tabletServerId,
+ 0,
+ Arrays.asList(1, 2),
+ Collections.emptyList(),
+ 0,
+ 1));
}
}
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrTest.java
index 1779b96f8..6ab4ca69a 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/AdjustIsrTest.java
@@ -133,7 +133,8 @@ public class AdjustIsrTest extends ReplicaTestBase {
// To mock we prepare an isr shrink in Replica#maybeShrinkIsr();
IsrState.PendingShrinkIsrState pendingShrinkIsrState =
replica.prepareIsrShrink(
- new IsrState.CommittedIsrState(Arrays.asList(1, 2, 3)),
+ new IsrState.CommittedIsrState(
+ Arrays.asList(1, 2, 3),
Collections.emptyList()),
Arrays.asList(1, 2),
Collections.singletonList(3));
@@ -161,7 +162,8 @@ public class AdjustIsrTest extends ReplicaTestBase {
// To mock we prepare an isr shrink in Replica#maybeShrinkIsr();
IsrState.PendingShrinkIsrState pendingShrinkIsrState =
replica.prepareIsrShrink(
- new IsrState.CommittedIsrState(Arrays.asList(1, 2, 3)),
+ new IsrState.CommittedIsrState(
+ Arrays.asList(1, 2, 3),
Collections.emptyList()),
Arrays.asList(1, 2),
Collections.singletonList(3));
@@ -176,6 +178,6 @@ public class AdjustIsrTest extends ReplicaTestBase {
.hasMessage(
"Rejecting adjustIsr request for table bucket "
+ "TableBucket{tableId=150001, bucket=1}
because it specified ineligible replicas [2] "
- + "in the new ISR LeaderAndIsr{leader=1,
leaderEpoch=0, isr=[1, 2], coordinatorEpoch=0, bucketEpoch=0}");
+ + "in the new ISR LeaderAndIsr{leader=1,
leaderEpoch=0, isr=[1, 2], standbyReplicas=[], coordinatorEpoch=0,
bucketEpoch=0}");
}
}
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/HighWatermarkPersistenceTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/HighWatermarkPersistenceTest.java
index b093787f0..a520f9dad 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/HighWatermarkPersistenceTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/HighWatermarkPersistenceTest.java
@@ -105,6 +105,7 @@ final class HighWatermarkPersistenceTest extends
ReplicaTestBase {
TABLET_SERVER_ID,
LeaderAndIsr.INITIAL_LEADER_EPOCH,
Collections.singletonList(TABLET_SERVER_ID),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
LeaderAndIsr.INITIAL_BUCKET_EPOCH))),
result -> {});
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/KvRecoverFromRemoteLogITCase.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/KvRecoverFromRemoteLogITCase.java
index a070870eb..47c5ec88f 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/KvRecoverFromRemoteLogITCase.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/KvRecoverFromRemoteLogITCase.java
@@ -43,6 +43,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -215,6 +216,7 @@ class KvRecoverFromRemoteLogITCase {
followerToPromote,
currentLeaderAndIsr.leaderEpoch() + 1,
currentLeaderAndIsr.isr(),
+ Collections.emptyList(),
currentLeaderAndIsr.coordinatorEpoch(),
currentLeaderAndIsr.bucketEpoch() + 1);
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaManagerTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaManagerTest.java
index 0d9f5bc56..5120bf6a6 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaManagerTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaManagerTest.java
@@ -1458,6 +1458,7 @@ class ReplicaManagerTest extends ReplicaTestBase {
TABLET_SERVER_ID,
1,
Arrays.asList(1, 2, 3),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
INITIAL_BUCKET_EPOCH))),
future::complete);
@@ -1478,6 +1479,7 @@ class ReplicaManagerTest extends ReplicaTestBase {
TABLET_SERVER_ID,
INITIAL_LEADER_EPOCH,
Arrays.asList(1, 2, 3),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
INITIAL_BUCKET_EPOCH))),
future::complete);
@@ -1512,6 +1514,7 @@ class ReplicaManagerTest extends ReplicaTestBase {
TABLET_SERVER_ID,
1,
Arrays.asList(1, 2, 3),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
INITIAL_BUCKET_EPOCH))),
future::complete);
@@ -1543,6 +1546,7 @@ class ReplicaManagerTest extends ReplicaTestBase {
TABLET_SERVER_ID,
2,
Arrays.asList(1, 2, 3),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
INITIAL_BUCKET_EPOCH))),
future::complete);
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaTest.java
index 04331fb6a..4eefc0e2f 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaTest.java
@@ -801,6 +801,7 @@ final class ReplicaTest extends ReplicaTestBase {
TABLET_SERVER_ID,
leaderEpoch,
Collections.singletonList(TABLET_SERVER_ID),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
// we also use the leader epoch as bucket epoch
leaderEpoch)));
@@ -818,6 +819,7 @@ final class ReplicaTest extends ReplicaTestBase {
TABLET_SERVER_ID,
leaderEpoch,
Collections.singletonList(TABLET_SERVER_ID),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
// we also use the leader epoch as bucket epoch
leaderEpoch)));
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaTestBase.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaTestBase.java
index 8c2f772c3..f6b2e01c8 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaTestBase.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaTestBase.java
@@ -396,6 +396,7 @@ public class ReplicaTestBase {
TABLET_SERVER_ID,
INITIAL_LEADER_EPOCH,
isr,
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
INITIAL_BUCKET_EPOCH))));
}
@@ -437,6 +438,7 @@ public class ReplicaTestBase {
TABLET_SERVER_ID,
leaderEpoch,
isr,
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
// use leader epoch as bucket epoch
leaderEpoch))));
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherITCase.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherITCase.java
index e2bb80f96..f94db7184 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherITCase.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherITCase.java
@@ -315,6 +315,7 @@ public class ReplicaFetcherITCase {
currentLeaderAndIsr.leader(),
currentLeaderAndIsr.leaderEpoch() + 1,
currentLeaderAndIsr.isr(),
+ Collections.emptyList(),
currentLeaderAndIsr.coordinatorEpoch(),
currentLeaderAndIsr.bucketEpoch());
FLUSS_CLUSTER_EXTENSION.notifyLeaderAndIsr(
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherManagerTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherManagerTest.java
index 4329ff6b3..15be1f2b0 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherManagerTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherManagerTest.java
@@ -91,6 +91,7 @@ class ReplicaFetcherManagerTest extends ReplicaTestBase {
leader.id(),
LeaderAndIsr.INITIAL_LEADER_EPOCH,
Arrays.asList(leader.id(),
TABLET_SERVER_ID),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
LeaderAndIsr.INITIAL_BUCKET_EPOCH))),
result -> {});
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherThreadTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherThreadTest.java
index 556c5bea4..ff089cb4a 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherThreadTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/replica/fetcher/ReplicaFetcherThreadTest.java
@@ -477,6 +477,7 @@ public class ReplicaFetcherThreadTest {
leaderServerId,
INITIAL_LEADER_EPOCH,
Arrays.asList(leaderServerId,
followerServerId),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
INITIAL_BUCKET_EPOCH))),
result -> {});
@@ -491,6 +492,7 @@ public class ReplicaFetcherThreadTest {
leaderServerId,
INITIAL_LEADER_EPOCH,
Arrays.asList(leaderServerId,
followerServerId),
+ Collections.emptyList(),
INITIAL_COORDINATOR_EPOCH,
INITIAL_BUCKET_EPOCH))),
result -> {});
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/tablet/TabletServerShutdownITCase.java
b/fluss-server/src/test/java/org/apache/fluss/server/tablet/TabletServerShutdownITCase.java
index 68ec0d911..95e66f252 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/tablet/TabletServerShutdownITCase.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/tablet/TabletServerShutdownITCase.java
@@ -217,7 +217,9 @@ public class TabletServerShutdownITCase {
// should be no leader
LeaderAndIsr expectedLeaderAndIsr2 =
expectedLeaderAndIsr1.newLeaderAndIsr(
- LeaderAndIsr.NO_LEADER,
Collections.singletonList(leader));
+ LeaderAndIsr.NO_LEADER,
+ Collections.singletonList(leader),
+ Collections.emptyList());
retry(
Duration.ofMinutes(1),
() ->
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/tablet/TabletServiceITCase.java
b/fluss-server/src/test/java/org/apache/fluss/server/tablet/TabletServiceITCase.java
index 7bdd78107..17b436e1a 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/tablet/TabletServiceITCase.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/tablet/TabletServiceITCase.java
@@ -866,6 +866,7 @@ public class TabletServiceITCase {
leader,
1,
originLeaderAndIsr.isr(),
+
originLeaderAndIsr.standbyReplicas(),
originLeaderAndIsr.coordinatorEpoch(),
originLeaderAndIsr.bucketEpoch())))
.get();
@@ -909,6 +910,7 @@ public class TabletServiceITCase {
leader,
2,
originLeaderAndIsr.isr(),
+
originLeaderAndIsr.standbyReplicas(),
originLeaderAndIsr.coordinatorEpoch(),
originLeaderAndIsr.bucketEpoch())))
.get();
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/testutils/FlussClusterExtension.java
b/fluss-server/src/test/java/org/apache/fluss/server/testutils/FlussClusterExtension.java
index 74a041596..00a2344a6 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/testutils/FlussClusterExtension.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/testutils/FlussClusterExtension.java
@@ -921,6 +921,11 @@ public final class FlussClusterExtension
return waitLeaderAndIsrReady(tb).leader();
}
+ public int waitAndGetStandby(TableBucket tb) {
+ List<Integer> standbyReplicas =
waitLeaderAndIsrReady(tb).standbyReplicas();
+ return standbyReplicas.get(0);
+ }
+
public LeaderAndIsr waitLeaderAndIsrReady(TableBucket tb) {
ZooKeeperClient zkClient = getZooKeeperClient();
return waitValue(
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/zk/ZooKeeperClientTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/zk/ZooKeeperClientTest.java
index 16f59ddd8..a94cf8e0d 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/zk/ZooKeeperClientTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/zk/ZooKeeperClientTest.java
@@ -202,8 +202,11 @@ class ZooKeeperClientTest {
assertThat(zookeeperClient.getLeaderAndIsr(tableBucket2)).isEmpty();
// try to register bucket leaderAndIsr
- LeaderAndIsr leaderAndIsr1 = new LeaderAndIsr(1, 10, Arrays.asList(1,
2, 3), 0, 1000);
- LeaderAndIsr leaderAndIsr2 = new LeaderAndIsr(2, 10, Arrays.asList(4,
5, 6), 0, 1000);
+ LeaderAndIsr leaderAndIsr1 =
+ new LeaderAndIsr(
+ 1, 10, Arrays.asList(1, 2, 3),
Collections.singletonList(2), 0, 1000);
+ LeaderAndIsr leaderAndIsr2 =
+ new LeaderAndIsr(2, 10, Arrays.asList(4, 5, 6),
Collections.emptyList(), 0, 1000);
zookeeperClient.registerLeaderAndIsr(
tableBucket1, leaderAndIsr1,
zkEpoch.getCoordinatorEpochZkVersion());
@@ -215,7 +218,9 @@ class ZooKeeperClientTest {
.containsValues(leaderAndIsr1, leaderAndIsr2);
// test update
- leaderAndIsr1 = new LeaderAndIsr(2, 20, Collections.emptyList(), 0,
2000);
+ leaderAndIsr1 =
+ new LeaderAndIsr(
+ 2, 20, Collections.emptyList(),
Collections.singletonList(3), 0, 2000);
zookeeperClient.updateLeaderAndIsr(
tableBucket1, leaderAndIsr1,
zkEpoch.getCoordinatorEpochZkVersion());
assertThat(zookeeperClient.getLeaderAndIsr(tableBucket1)).hasValue(leaderAndIsr1);
@@ -234,7 +239,13 @@ class ZooKeeperClientTest {
TableBucket tableBucket =
isPartitionTable ? new TableBucket(1, 2L, i) : new
TableBucket(1, i);
LeaderAndIsr leaderAndIsr =
- new LeaderAndIsr(i, 10, Arrays.asList(i + 1, i + 2, i +
3), 0, 1000);
+ new LeaderAndIsr(
+ i,
+ 10,
+ Arrays.asList(i + 1, i + 2, i + 3),
+ Collections.emptyList(),
+ 0,
+ 1000);
leaderAndIsrList.add(leaderAndIsr);
RegisterTableBucketLeadAndIsrInfo info =
isPartitionTable
@@ -271,7 +282,8 @@ class ZooKeeperClientTest {
LeaderAndIsr adjustLeaderAndIsr =
originalLeaderAndIsr.newLeaderAndIsr(
LeaderAndIsr.NO_LEADER,
-
originalLeaderAndIsr.isr().subList(0, 1));
+
originalLeaderAndIsr.isr().subList(0, 1),
+ Collections.emptyList());
leaderAndIsrUpdateList.add(adjustLeaderAndIsr);
entry.setValue(adjustLeaderAndIsr);
});
@@ -295,7 +307,13 @@ class ZooKeeperClientTest {
for (int i = 0; i < totalCount; i++) {
TableBucket tableBucket = new TableBucket(1, i);
LeaderAndIsr leaderAndIsr =
- new LeaderAndIsr(i, 10, Arrays.asList(i + 1, i + 2, i +
3), 0, 1000);
+ new LeaderAndIsr(
+ i,
+ 10,
+ Arrays.asList(i + 1, i + 2, i + 3),
+ Collections.emptyList(),
+ 0,
+ 1000);
leaderAndIsrList.put(tableBucket, leaderAndIsr);
zookeeperClient.registerLeaderAndIsr(
tableBucket, leaderAndIsr,
zkEpoch.getCoordinatorEpochZkVersion());
@@ -313,6 +331,7 @@ class ZooKeeperClientTest {
old.leader() + 1,
old.leaderEpoch() + 1,
old.isr(),
+ Collections.emptyList(),
old.coordinatorEpoch(),
old.bucketEpoch() + 1);
}));
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/zk/data/LeaderAndIsrJsonSerdeTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/zk/data/LeaderAndIsrJsonSerdeTest.java
index 128b14a18..f71a98d98 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/zk/data/LeaderAndIsrJsonSerdeTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/zk/data/LeaderAndIsrJsonSerdeTest.java
@@ -17,12 +17,17 @@
package org.apache.fluss.server.zk.data;
+import
org.apache.fluss.shaded.jackson2.com.fasterxml.jackson.databind.JsonNode;
+import
org.apache.fluss.shaded.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.fluss.utils.json.JsonSerdeTestBase;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
-/** Test for {@link org.apache.fluss.server.zk.data.LeaderAndIsrJsonSerde}. */
+/** Test for {@link LeaderAndIsrJsonSerde}. */
public class LeaderAndIsrJsonSerdeTest extends JsonSerdeTestBase<LeaderAndIsr>
{
LeaderAndIsrJsonSerdeTest() {
@@ -31,16 +36,37 @@ public class LeaderAndIsrJsonSerdeTest extends
JsonSerdeTestBase<LeaderAndIsr> {
@Override
protected LeaderAndIsr[] createObjects() {
- LeaderAndIsr leaderAndIsr1 = new LeaderAndIsr(1, 10, Arrays.asList(1,
2, 3), 100, 1000);
- LeaderAndIsr leaderAndIsr2 = new LeaderAndIsr(2, 20,
Collections.emptyList(), 200, 2000);
- return new LeaderAndIsr[] {leaderAndIsr1, leaderAndIsr2};
+ LeaderAndIsr leaderAndIsr1 =
+ new LeaderAndIsr(1, 10, Arrays.asList(1, 2, 3),
Collections.emptyList(), 100, 1000);
+ LeaderAndIsr leaderAndIsr2 =
+ new LeaderAndIsr(
+ 2, 20, Collections.emptyList(),
Collections.emptyList(), 200, 2000);
+ LeaderAndIsr leaderAndIsr3 =
+ new LeaderAndIsr(
+ 2, 20, Arrays.asList(1, 2, 3),
Collections.singletonList(3), 200, 2000);
+ return new LeaderAndIsr[] {leaderAndIsr1, leaderAndIsr2,
leaderAndIsr3};
}
@Override
protected String[] expectedJsons() {
return new String[] {
-
"{\"version\":1,\"leader\":1,\"leader_epoch\":10,\"isr\":[1,2,3],\"coordinator_epoch\":100,\"bucket_epoch\":1000}",
-
"{\"version\":1,\"leader\":2,\"leader_epoch\":20,\"isr\":[],\"coordinator_epoch\":200,\"bucket_epoch\":2000}"
+
"{\"version\":2,\"leader\":1,\"leader_epoch\":10,\"isr\":[1,2,3],\"coordinator_epoch\":100,\"bucket_epoch\":1000,\"standby_replicas\":[]}",
+
"{\"version\":2,\"leader\":2,\"leader_epoch\":20,\"isr\":[],\"coordinator_epoch\":200,\"bucket_epoch\":2000,\"standby_replicas\":[]}",
+
"{\"version\":2,\"leader\":2,\"leader_epoch\":20,\"isr\":[1,2,3],\"coordinator_epoch\":200,\"bucket_epoch\":2000,\"standby_replicas\":[3]}"
};
}
+
+ @Test
+ void testCompatibility() throws Exception {
+ // test compatibility with version 1
+ JsonNode jsonInVersion1 =
+ new ObjectMapper()
+ .readTree(
+
"{\"version\":1,\"leader\":1,\"leader_epoch\":10,\"isr\":[1,2,3],\"coordinator_epoch\":100,\"bucket_epoch\":1000}"
+ .getBytes(StandardCharsets.UTF_8));
+ LeaderAndIsr leaderAndIsr =
LeaderAndIsrJsonSerde.INSTANCE.deserialize(jsonInVersion1);
+ LeaderAndIsr expectedLeaderAndIsr =
+ new LeaderAndIsr(1, 10, Arrays.asList(1, 2, 3),
Collections.emptyList(), 100, 1000);
+ assertEquals(leaderAndIsr, expectedLeaderAndIsr);
+ }
}