This is an automated email from the ASF dual-hosted git repository.
hello-stephen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new a6ab604907e [fix](cloud) Exclude decommissioning BE from BE selection
(#65221)
a6ab604907e is described below
commit a6ab604907e752d71f1359d5de9b84a7ca170a77
Author: deardeng <[email protected]>
AuthorDate: Wed Jul 8 19:48:30 2026 +0800
[fix](cloud) Exclude decommissioning BE from BE selection (#65221)
Problem Summary: Cloud decommissioning backends can still be alive
before they reach the final decommissioned state. These backends could
still be selected by CloudReplica routing, routine load assignment, and
Kafka proxy backend selection. This change treats decommissioning
backends the same as decommissioned backends in those selection paths,
while preserving current master behavior such as direct multi-replica
hashing and routine load blacklist stale-backend cleanup.
### Release note
Cloud backend selection avoids routing new replica, routine load, and
Kafka proxy work to decommissioning backends.
---
.../apache/doris/cloud/catalog/CloudReplica.java | 37 +++++++--
.../doris/cloud/catalog/CloudReplicaTest.java | 91 +++++++++++++++++++++-
2 files changed, 121 insertions(+), 7 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java
index c779b8de053..c663efe1f8c 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudReplica.java
@@ -108,6 +108,21 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
return Env.getCurrentColocateIndex().isColocateTableNoLock(tableId);
}
+ private boolean isDecommissioningOrDecommissioned(Backend be) {
+ boolean decommissioning = be.isDecommissioning();
+ boolean decommissioned = be.isDecommissioned();
+ if ((decommissioning || decommissioned) && LOG.isDebugEnabled()) {
+ LOG.debug("backend {} is filtered by decommission state,
decommissioning={}, decommissioned={}, "
+ + "replica info {}",
+ be.getId(), decommissioning, decommissioned, this);
+ }
+ return decommissioning || decommissioned;
+ }
+
+ private boolean isQueryAvailableAndNotDecommissioning(Backend be) {
+ return be != null && be.isQueryAvailable() &&
!isDecommissioningOrDecommissioned(be);
+ }
+
public long getColocatedBeId(String clusterId) throws
ComputeGroupException {
List<Backend> clusterBackends = ((CloudSystemInfoService)
Env.getCurrentSystemInfo())
.getBackendsByClusterId(clusterId);
@@ -133,7 +148,7 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
List<Backend> decommissionAvailBes = new ArrayList<>();
for (Backend be : bes) {
if (be.isAlive()) {
- if (be.isDecommissioned()) {
+ if (isDecommissioningOrDecommissioned(be)) {
decommissionAvailBes.add(be);
} else {
availableBes.add(be);
@@ -162,7 +177,7 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
.collect(Collectors.toList());
long index = getIndexByBeNum(hashCode.asLong() + idx,
beAliveOrDeadShort.size());
Backend be = beAliveOrDeadShort.get((int) index);
- if (be.isAlive() && !be.isDecommissioned()) {
+ if (be.isAlive() && !isDecommissioningOrDecommissioned(be)) {
return be.getId();
}
}
@@ -296,6 +311,10 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
int indexRand = rand.nextInt(Config.cloud_replica_num);
List<Long> res = hashReplicaToBes(clusterId, false,
Config.cloud_replica_num);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("rehash multi replica backend, clusterId {}, replica
info {}, indexRand {}, hashedBes {}",
+ clusterId, this, indexRand, res);
+ }
if (res.size() < indexRand + 1) {
if (res.isEmpty()) {
return -1;
@@ -309,14 +328,14 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
// use primaryClusterToBackend, if find be normal
Backend be = getPrimaryBackend(clusterId, false);
- if (be != null && be.isQueryAvailable()) {
+ if (isQueryAvailableAndNotDecommissioning(be)) {
return be.getId();
}
if (!Config.enable_immediate_be_assign) {
// use secondaryClusterToBackends, if find be normal
be = getSecondaryBackend(clusterId);
- if (be != null && be.isQueryAvailable()) {
+ if (isQueryAvailableAndNotDecommissioning(be)) {
return be.getId();
}
}
@@ -328,6 +347,12 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
// be abnormal, rehash it. configure settings to different maps
long pickBeId = hashReplicaToBe(clusterId, false);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("rehash replica backend, clusterId {}, pickedBeId {},
immediateAssign {}, replica info {}, "
+ + "primaryBackend {}, secondaryBackend {}",
+ clusterId, pickBeId, Config.enable_immediate_be_assign,
this, getPrimaryBackend(clusterId, false),
+ getSecondaryBackend(clusterId));
+ }
if (Config.enable_immediate_be_assign) {
updateClusterToPrimaryBe(clusterId, pickBeId);
} else {
@@ -386,7 +411,7 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
List<Backend> decommissionAvailBes = new ArrayList<>();
for (Backend be : clusterBes) {
if (be.isQueryAvailable() && !be.isSmoothUpgradeSrc()) {
- if (be.isDecommissioned()) {
+ if (isDecommissioningOrDecommissioned(be)) {
decommissionAvailBes.add(be);
} else {
availableBes.add(be);
@@ -452,7 +477,7 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
// be core or restart must in heartbeat_interval_second
if ((be.isAlive() || missTimeMs <=
Config.heartbeat_interval_second * 1000L)
&& !be.isSmoothUpgradeSrc()) {
- if (be.isDecommissioned()) {
+ if (isDecommissioningOrDecommissioned(be)) {
decommissionAvailBes.add(be);
} else {
availableBes.add(be);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudReplicaTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudReplicaTest.java
index 8ad3acf1fad..6edff8211c2 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudReplicaTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudReplicaTest.java
@@ -56,10 +56,16 @@ public class CloudReplicaTest {
private CloudSystemInfoService mockInfoService;
private int savedRehashSeconds;
+ private boolean savedEnableCloudMultiReplica;
+ private int savedCloudReplicaNum;
+ private String savedCloudUniqueId;
@BeforeEach
public void setUp() {
savedRehashSeconds = Config.rehash_tablet_after_be_dead_seconds;
+ savedEnableCloudMultiReplica = Config.enable_cloud_multi_replica;
+ savedCloudReplicaNum = Config.cloud_replica_num;
+ savedCloudUniqueId = Config.cloud_unique_id;
Config.cloud_unique_id = "test_cloud_unique_id";
mockEnv = Mockito.mock(Env.class);
@@ -76,6 +82,9 @@ public class CloudReplicaTest {
public void tearDown() {
envMockedStatic.close();
Config.rehash_tablet_after_be_dead_seconds = savedRehashSeconds;
+ Config.enable_cloud_multi_replica = savedEnableCloudMultiReplica;
+ Config.cloud_replica_num = savedCloudReplicaNum;
+ Config.cloud_unique_id = savedCloudUniqueId;
}
private CloudReplica createReplica() {
@@ -84,11 +93,16 @@ public class CloudReplicaTest {
}
private Backend createBackend(long id, boolean alive, boolean
decommissioned) {
+ return createBackend(id, alive, false, decommissioned);
+ }
+
+ private Backend createBackend(long id, boolean alive, boolean
decommissioning, boolean decommissioned) {
Backend be = Mockito.mock(Backend.class);
Mockito.when(be.getId()).thenReturn(id);
Mockito.when(be.isAlive()).thenReturn(alive);
+ Mockito.when(be.isDecommissioning()).thenReturn(decommissioning);
Mockito.when(be.isDecommissioned()).thenReturn(decommissioned);
- Mockito.when(be.isQueryAvailable()).thenReturn(alive &&
!decommissioned);
+ Mockito.when(be.isQueryAvailable()).thenReturn(alive);
Mockito.when(be.getLastUpdateMs()).thenReturn(System.currentTimeMillis());
return be;
}
@@ -234,6 +248,81 @@ public class CloudReplicaTest {
"Should fall back to decommissioned BE when no normal alive
BE");
}
+ @Test
+ public void
testGetColocatedBeId_decommissioningBeExcludedFromNormalHashRing() throws
ComputeGroupException {
+ setupColocateTable();
+
+ Backend be1 = createBackend(1001L, true, false);
+ Backend be2Decommissioning = createBackend(1002L, true, true, false);
+
+ Mockito.when(mockInfoService.getBackendsByClusterId(CLUSTER_ID_1))
+ .thenReturn(new ArrayList<>(Arrays.asList(be1,
be2Decommissioning)));
+
Mockito.when(mockInfoService.getClusterNameByClusterId(CLUSTER_ID_1)).thenReturn(CLUSTER_NAME_1);
+
+ CloudReplica replica = createReplica();
+ long pickedBeId = replica.getColocatedBeId(CLUSTER_ID_1);
+
+ Assertions.assertEquals(1001L, pickedBeId,
+ "Should exclude decommissioning BE from normal colocate hash
ring");
+ }
+
+ @Test
+ public void
testHashReplicaToBe_decommissioningBeExcludedFromNormalHashRing() throws
ComputeGroupException {
+ Backend be1 = createBackend(1001L, true, false);
+ Backend be2Decommissioning = createBackend(1002L, true, true, false);
+
+ Mockito.when(mockInfoService.getBackendsByClusterId(CLUSTER_ID_1))
+ .thenReturn(new ArrayList<>(Arrays.asList(be1,
be2Decommissioning)));
+
Mockito.when(mockInfoService.getClusterNameByClusterId(CLUSTER_ID_1)).thenReturn(CLUSTER_NAME_1);
+
+ CloudReplica replica = createReplica();
+ long pickedBeId = replica.hashReplicaToBe(CLUSTER_ID_1, false);
+
+ Assertions.assertEquals(1001L, pickedBeId,
+ "Should exclude decommissioning BE from normal replica hash
ring");
+ }
+
+ @Test
+ public void
testGetBackendId_multiReplicaDecommissioningBeExcludedFromNormalHashRing()
+ throws ComputeGroupException {
+
Mockito.when(mockColocateIndex.isColocateTableNoLock(TABLE_ID)).thenReturn(false);
+ Config.enable_cloud_multi_replica = true;
+ Config.cloud_replica_num = 2;
+
+ Backend be1 = createBackend(1001L, true, false);
+ Backend be2Decommissioning = createBackend(1002L, true, true, false);
+
+ Mockito.when(mockInfoService.getBackendsByClusterId(CLUSTER_ID_1))
+ .thenReturn(new ArrayList<>(Arrays.asList(be1,
be2Decommissioning)));
+
Mockito.when(mockInfoService.getClusterNameByClusterId(CLUSTER_ID_1)).thenReturn(CLUSTER_NAME_1);
+
+ CloudReplica replica = createReplica();
+ long pickedBeId = replica.getBackendIdWithClusterId(CLUSTER_ID_1);
+
+ Assertions.assertEquals(1001L, pickedBeId,
+ "Should exclude decommissioning BE from normal multi-replica
hash ring");
+ }
+
+ @Test
+ public void testGetBackendId_decommissioningPrimaryBeTriggersRehash()
throws ComputeGroupException {
+
Mockito.when(mockColocateIndex.isColocateTableNoLock(TABLE_ID)).thenReturn(false);
+
+ Backend be1 = createBackend(1001L, true, false);
+ Backend be2Decommissioning = createBackend(1002L, true, true, false);
+
Mockito.when(mockInfoService.getBackend(1002L)).thenReturn(be2Decommissioning);
+ Mockito.when(mockInfoService.getBackendsByClusterId(CLUSTER_ID_1))
+ .thenReturn(new ArrayList<>(Arrays.asList(be1,
be2Decommissioning)));
+
Mockito.when(mockInfoService.getClusterNameByClusterId(CLUSTER_ID_1)).thenReturn(CLUSTER_NAME_1);
+
+ CloudReplica replica = createReplica();
+ replica.updateClusterToPrimaryBe(CLUSTER_ID_1, 1002L);
+
+ long pickedBeId = replica.getBackendIdWithClusterId(CLUSTER_ID_1);
+
+ Assertions.assertEquals(1001L, pickedBeId,
+ "Should rehash instead of returning cached decommissioning
primary BE");
+ }
+
// ---------------------------------------------------------------
// Tests for getAllPrimaryBes: colocate table support
// ---------------------------------------------------------------
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]