[Enhancement] Force drop decommissioned backend if all the tablets in recycle bin (backport #62781) (#63156)

Signed-off-by: gengjun-git <gengjun@starrocks.com>
Co-authored-by: gengjun-git <gengjun@starrocks.com>
This commit is contained in:
mergify[bot] 2025-09-16 02:39:21 +00:00 committed by GitHub
parent c0e4a1337b
commit 631312127b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 541 additions and 3 deletions

View File

@ -37,11 +37,15 @@ package com.starrocks.alter;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.starrocks.catalog.BrokerMgr;
import com.starrocks.catalog.CatalogRecycleBin;
import com.starrocks.catalog.Database;
import com.starrocks.catalog.OlapTable;
import com.starrocks.catalog.PartitionInfo;
import com.starrocks.catalog.Replica;
import com.starrocks.catalog.Replica.ReplicaState;
import com.starrocks.catalog.Table;
import com.starrocks.catalog.TabletInvertedIndex;
import com.starrocks.catalog.TabletMeta;
import com.starrocks.common.Config;
import com.starrocks.common.DdlException;
import com.starrocks.common.ErrorReport;
@ -82,6 +86,7 @@ import org.apache.logging.log4j.Logger;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@ -93,6 +98,8 @@ import java.util.stream.Collectors;
*/
public class SystemHandler extends AlterHandler {
private static final Logger LOG = LogManager.getLogger(SystemHandler.class);
private static final long RECYCLE_BIN_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 min
private long lastRecycleBinCheckTime = 0L;
public SystemHandler() {
super("cluster");
@ -355,11 +362,15 @@ public class SystemHandler extends AlterHandler {
}
List<Long> backendTabletIds = invertedIndex.getTabletIdsByBackendId(beId);
if (backendTabletIds.isEmpty()) {
if (canDropBackend(backendTabletIds)) {
if (Config.drop_backend_after_decommission) {
try {
systemInfoService.dropBackend(beId);
LOG.info("no tablet on decommission backend {}, drop it", beId);
if (backendTabletIds.isEmpty()) {
LOG.info("no tablet on decommission backend {}, drop it", beId);
} else {
LOG.info("force drop decommission backend {}, the tablets on it are all in recycle bin", beId);
}
} catch (DdlException e) {
// does not matter, maybe backend not exists
LOG.info("backend {} drop failed after decommission {}", beId, e.getMessage());
@ -373,6 +384,75 @@ public class SystemHandler extends AlterHandler {
}
}
/**
* If the following conditions are met, it can be forced to drop the backend
* 1. All the tablets are in recycle bin.
* 2. All the replication number of tablets is bigger than the retained backend number
* (which means there is no backend to migrate, so decommission is blocked),
* and at least one healthy replica on retained backend.
* 3. There are at least 1 available backend.
*/
protected boolean canDropBackend(List<Long> backendTabletIds) {
if (backendTabletIds.isEmpty()) {
return true;
}
// There is only on replica for shared data mode, so tablets can be migrated to other backends.
if (RunMode.isSharedDataMode()) {
return false;
}
if (lastRecycleBinCheckTime + RECYCLE_BIN_CHECK_INTERVAL > System.currentTimeMillis()) {
return false;
}
lastRecycleBinCheckTime = System.currentTimeMillis();
SystemInfoService systemInfoService = GlobalStateMgr.getCurrentState().getNodeMgr().getClusterInfo();
int availableBECnt = systemInfoService.getAvailableBackends().size();
if (availableBECnt < 1) {
return false;
}
TabletInvertedIndex invertedIndex = GlobalStateMgr.getCurrentState().getTabletInvertedIndex();
CatalogRecycleBin recycleBin = GlobalStateMgr.getCurrentState().getRecycleBin();
List<Backend> retainedBackends = systemInfoService.getRetainedBackends();
int retainedHostCnt = (int) retainedBackends.stream().map(Backend::getHost).distinct().count();
Set<Long> retainedBackendIds = retainedBackends.stream().map(Backend::getId).collect(Collectors.toSet());
for (Long tabletId : backendTabletIds) {
TabletMeta tabletMeta = invertedIndex.getTabletMeta(tabletId);
if (tabletMeta == null) {
continue;
}
if (!recycleBin.isTabletInRecycleBin(tabletMeta)) {
return false;
}
Map<Long, Replica> replicas = invertedIndex.getReplicas(tabletId);
if (replicas == null) {
continue;
}
// It means the replica can be migrated to retained backends.
if (replicas.size() <= retainedHostCnt) {
return false;
}
// Make sure there is at least one normal replica on retained backends.
boolean hasNormalReplica = false;
for (Replica replica : replicas.values()) {
if (replica.getState() == ReplicaState.NORMAL && retainedBackendIds.contains(replica.getBackendId())) {
hasNormalReplica = true;
break;
}
}
if (!hasNormalReplica) {
return false;
}
}
return true;
}
@Override
public synchronized void cancel(CancelStmt stmt) throws DdlException {
CancelAlterSystemStmt cancelAlterSystemStmt = (CancelAlterSystemStmt) stmt;

View File

@ -242,7 +242,7 @@ public class CatalogRecycleBin extends FrontendDaemon implements Writable {
return null;
}
public PhysicalPartition getPhysicalPartition(long physicalPartitionId) {
public synchronized PhysicalPartition getPhysicalPartition(long physicalPartitionId) {
for (Partition partition : idToPartition.values().stream()
.map(RecyclePartitionInfo::getPartition)
.collect(Collectors.toList())) {
@ -1121,6 +1121,12 @@ public class CatalogRecycleBin extends FrontendDaemon implements Writable {
return Stream.of(dbInfos, tableInfos, partitionInfos).flatMap(Collection::stream).collect(Collectors.toList());
}
public synchronized boolean isTabletInRecycleBin(TabletMeta tabletMeta) {
return idToDatabase.containsKey(tabletMeta.getDbId()) ||
idToTableInfo.containsColumn(tabletMeta.getTableId()) ||
getPhysicalPartition(tabletMeta.getPhysicalPartitionId()) != null;
}
@VisibleForTesting
synchronized boolean isContainedInidToRecycleTime(long id) {
return idToRecycleTime.get(id) != null;

View File

@ -421,6 +421,15 @@ public class TabletInvertedIndex implements MemoryTrackable {
}
}
public Map<Long, Replica> getReplicas(long tabletId) {
readLock();
try {
return this.replicaMetaTable.get(tabletId);
} finally {
readUnlock();
}
}
// The caller should hold readLock.
public Map<Long, Replica> getReplicaMetaWithBackend(Long backendId) {
return row(backingReplicaMetaTable, backendId);

View File

@ -0,0 +1,365 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.alter;
import com.google.common.collect.Lists;
import com.starrocks.catalog.CatalogRecycleBin;
import com.starrocks.catalog.Replica;
import com.starrocks.catalog.TabletInvertedIndex;
import com.starrocks.catalog.TabletMeta;
import com.starrocks.server.RunMode;
import com.starrocks.system.Backend;
import com.starrocks.system.SystemInfoService;
import mockit.Mock;
import mockit.MockUp;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Test class for SystemHandler.canForceDrop method
*/
public class SystemHandlerCanForceDropTest {
private SystemHandler systemHandler;
@BeforeEach
public void setUp() throws Exception {
systemHandler = new SystemHandler();
}
@Test
public void testCanForceDropEmptyTablets() {
// Test case: empty tablet list should return true
List<Long> emptyTabletIds = new ArrayList<>();
boolean result = systemHandler.canDropBackend(emptyTabletIds);
Assertions.assertTrue(result, "Empty tablet list should return true");
}
@Test
public void testCanForceDropSharedDataMode() {
// Test case: shared data mode should return false
List<Long> tabletIds = Lists.newArrayList(1L, 2L, 3L);
// Mock RunMode to return shared data mode
new MockUp<RunMode>() {
@Mock
public static boolean isSharedDataMode() {
return true;
}
};
boolean result = systemHandler.canDropBackend(tabletIds);
Assertions.assertFalse(result, "Shared data mode should return false");
}
@Test
public void testCanForceDropRecycleBinInterval() throws Exception {
// Test case: within recycle bin check interval should return false
List<Long> tabletIds = Lists.newArrayList(1L, 2L, 3L);
// Mock RunMode to return shared nothing mode
new MockUp<RunMode>() {
@Mock
public static boolean isSharedDataMode() {
return false;
}
};
// First call to set lastRecycleBinCheckTime
systemHandler.canDropBackend(tabletIds);
// Second call within interval should return false
boolean result = systemHandler.canDropBackend(tabletIds);
Assertions.assertFalse(result, "Within recycle bin check interval should return false");
}
@Test
public void testCanForceDropNoAvailableBackends() throws Exception {
// Test case: no available backends should return false
List<Long> tabletIds = Lists.newArrayList(1L, 2L, 3L);
// Mock RunMode to return shared nothing mode
new MockUp<RunMode>() {
@Mock
public static boolean isSharedDataMode() {
return false;
}
};
// Mock SystemInfoService to return empty available backends
new MockUp<SystemInfoService>() {
@Mock
public List<Backend> getAvailableBackends() {
return new ArrayList<>();
}
};
boolean result = systemHandler.canDropBackend(tabletIds);
Assertions.assertFalse(result, "No available backends should return false");
}
@Test
public void testCanForceDropTabletNotInRecycleBin() throws Exception {
// Test case: tablet not in recycle bin should return false
List<Long> tabletIds = Lists.newArrayList(1L, 2L, 3L);
// Mock RunMode to return shared nothing mode
new MockUp<RunMode>() {
@Mock
public static boolean isSharedDataMode() {
return false;
}
};
// Mock SystemInfoService to return available backends
List<Backend> availableBackends = Lists.newArrayList(
new Backend(1L, "host1", 1000),
new Backend(2L, "host2", 1000)
);
new MockUp<SystemInfoService>() {
@Mock
public List<Backend> getAvailableBackends() {
return availableBackends;
}
@Mock
public List<Backend> getRetainedBackends() {
return Lists.newArrayList(availableBackends.get(0));
}
};
// Mock CatalogRecycleBin to return false for isTabletInRecycleBin
new MockUp<CatalogRecycleBin>() {
@Mock
public boolean isTabletInRecycleBin(TabletMeta tabletMeta) {
return false;
}
};
// Mock TabletInvertedIndex
mockTabletInvertedIndex();
boolean result = systemHandler.canDropBackend(tabletIds);
Assertions.assertFalse(result, "Tablet not in recycle bin should return false");
}
@Test
public void testCanForceDropInsufficientReplicas() throws Exception {
// Test case: insufficient replicas should return false
List<Long> tabletIds = Lists.newArrayList(1L, 2L, 3L);
// Mock RunMode to return shared nothing mode
new MockUp<RunMode>() {
@Mock
public static boolean isSharedDataMode() {
return false;
}
};
// Mock SystemInfoService to return available backends
List<Backend> availableBackends = Lists.newArrayList(
new Backend(1L, "host1", 1000),
new Backend(2L, "host2", 1000)
);
new MockUp<SystemInfoService>() {
@Mock
public List<Backend> getAvailableBackends() {
return availableBackends;
}
@Mock
public List<Backend> getRetainedBackends() {
return availableBackends;
}
};
// Mock CatalogRecycleBin to return true for isTabletInRecycleBin
new MockUp<CatalogRecycleBin>() {
@Mock
public boolean isTabletInRecycleBin(TabletMeta tabletMeta) {
return true;
}
};
// Mock TabletInvertedIndex with insufficient replicas
mockTabletInvertedIndexWithInsufficientReplicas();
boolean result = systemHandler.canDropBackend(tabletIds);
Assertions.assertFalse(result, "Insufficient replicas should return false");
}
@Test
public void testCanForceDropNoNormalReplica() throws Exception {
// Test case: no normal replica on retained backends should return false
List<Long> tabletIds = Lists.newArrayList(1L, 2L, 3L);
// Mock RunMode to return shared nothing mode
new MockUp<RunMode>() {
@Mock
public static boolean isSharedDataMode() {
return false;
}
};
// Mock SystemInfoService to return available backends
List<Backend> availableBackends = Lists.newArrayList(
new Backend(1L, "host1", 1000),
new Backend(2L, "host2", 1000)
);
new MockUp<SystemInfoService>() {
@Mock
public List<Backend> getAvailableBackends() {
return availableBackends;
}
@Mock
public List<Backend> getRetainedBackends() {
return Lists.newArrayList(availableBackends.get(0));
}
};
// Mock CatalogRecycleBin to return true for isTabletInRecycleBin
new MockUp<CatalogRecycleBin>() {
@Mock
public boolean isTabletInRecycleBin(TabletMeta tabletMeta) {
return true;
}
};
// Mock TabletInvertedIndex with no normal replica on retained backends
mockTabletInvertedIndexWithNoNormalReplica();
boolean result = systemHandler.canDropBackend(tabletIds);
Assertions.assertFalse(result, "No normal replica on retained backends should return false");
}
@Test
public void testCanForceDropSuccess() throws Exception {
// Test case: all conditions met should return true
List<Long> tabletIds = Lists.newArrayList(1L, 2L, 3L);
// Mock RunMode to return shared nothing mode
new MockUp<RunMode>() {
@Mock
public static boolean isSharedDataMode() {
return false;
}
};
// Mock SystemInfoService to return available backends
List<Backend> availableBackends = Lists.newArrayList(
new Backend(1L, "host1", 1000),
new Backend(2L, "host2", 1000)
);
new MockUp<SystemInfoService>() {
@Mock
public List<Backend> getAvailableBackends() {
return availableBackends;
}
@Mock
public List<Backend> getRetainedBackends() {
return Lists.newArrayList(availableBackends.get(0));
}
};
// Mock CatalogRecycleBin to return true for isTabletInRecycleBin
new MockUp<CatalogRecycleBin>() {
@Mock
public boolean isTabletInRecycleBin(TabletMeta tabletMeta) {
return true;
}
};
// Mock TabletInvertedIndex with sufficient replicas and normal replica on retained backends
mockTabletInvertedIndexWithSuccess();
boolean result = systemHandler.canDropBackend(tabletIds);
Assertions.assertTrue(result, "All conditions met should return true");
}
private void mockTabletInvertedIndex() {
new MockUp<TabletInvertedIndex>() {
@Mock
public TabletMeta getTabletMeta(long tabletId) {
return new TabletMeta(1L, 1L, 1L, 1L, null, false);
}
@Mock
public Map<Long, Replica> getReplicas(long tabletId) {
Map<Long, Replica> replicas = new HashMap<>();
replicas.put(1L, new Replica(1L, 1L, 1L, 1, 0L, 0L, Replica.ReplicaState.NORMAL, -1L, 1L));
return replicas;
}
};
}
private void mockTabletInvertedIndexWithInsufficientReplicas() {
new MockUp<TabletInvertedIndex>() {
@Mock
public TabletMeta getTabletMeta(long tabletId) {
return new TabletMeta(1L, 1L, 1L, 1L, null, false);
}
@Mock
public Map<Long, Replica> getReplicas(long tabletId) {
Map<Long, Replica> replicas = new HashMap<>();
replicas.put(1L, new Replica(1L, 1L, 1L, 1, 0L, 0L, Replica.ReplicaState.NORMAL, -1L, 1L));
return replicas;
}
};
}
private void mockTabletInvertedIndexWithNoNormalReplica() {
new MockUp<TabletInvertedIndex>() {
@Mock
public TabletMeta getTabletMeta(long tabletId) {
return new TabletMeta(1L, 1L, 1L, 1L, null, false);
}
@Mock
public Map<Long, Replica> getReplicas(long tabletId) {
Map<Long, Replica> replicas = new HashMap<>();
// Create replica on non-retained backend (backendId = 2)
replicas.put(1L, new Replica(1L, 2L, 1L, 1, 0L, 0L, Replica.ReplicaState.NORMAL, -1L, 1L));
return replicas;
}
};
}
private void mockTabletInvertedIndexWithSuccess() {
new MockUp<TabletInvertedIndex>() {
@Mock
public TabletMeta getTabletMeta(long tabletId) {
return new TabletMeta(1L, 1L, 1L, 1L, null, false);
}
@Mock
public Map<Long, Replica> getReplicas(long tabletId) {
Map<Long, Replica> replicas = new HashMap<>();
// Create replica on retained backend (backendId = 1)
replicas.put(1L, new Replica(1L, 1L, 1L, 1, 0L, 0L, Replica.ReplicaState.NORMAL, -1L, 1L));
replicas.put(2L, new Replica(2L, 2L, 1L, 1, 0L, 0L, Replica.ReplicaState.NORMAL, -1L, 1L));
return replicas;
}
};
}
}

View File

@ -0,0 +1,78 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.catalog;
import com.starrocks.thrift.TStorageMedium;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Map;
/**
* Unit tests for TabletInvertedIndex class
*/
public class TabletInvertedIndexTest {
private TabletInvertedIndex tabletInvertedIndex;
private TabletMeta tabletMeta;
private Replica replica1;
private Replica replica2;
private Replica replica3;
@BeforeEach
public void setUp() {
tabletInvertedIndex = new TabletInvertedIndex();
// Create test tablet meta
tabletMeta = new TabletMeta(1L, 2L, 3L, 4L, TStorageMedium.HDD);
// Create test replicas
replica1 = new Replica(100L, 1000L, 1L, 123, 0L, 0L,
Replica.ReplicaState.NORMAL, -1L, 1L);
replica2 = new Replica(101L, 1001L, 1L, 123, 0L, 0L,
Replica.ReplicaState.NORMAL, -1L, 1L);
replica3 = new Replica(102L, 1002L, 1L, 123, 0L, 0L,
Replica.ReplicaState.NORMAL, -1L, 1L);
}
@Test
public void testGetReplicas_WithReplicas() {
// Given: Add tablet and replicas
long tabletId = 1000L;
tabletInvertedIndex.addTablet(tabletId, tabletMeta);
tabletInvertedIndex.addReplica(tabletId, replica1);
tabletInvertedIndex.addReplica(tabletId, replica2);
tabletInvertedIndex.addReplica(tabletId, replica3);
// When: Get replicas for the tablet
Map<Long, Replica> replicas = tabletInvertedIndex.getReplicas(tabletId);
// Then: Verify the result
Assertions.assertNotNull(replicas, "Replicas map should not be null");
Assertions.assertEquals(3, replicas.size(), "Should have 3 replicas");
// Verify each replica is present
Assertions.assertTrue(replicas.containsKey(1000L), "Should contain replica on backend 1000");
Assertions.assertTrue(replicas.containsKey(1001L), "Should contain replica on backend 1001");
Assertions.assertTrue(replicas.containsKey(1002L), "Should contain replica on backend 1002");
// Verify replica details
Assertions.assertEquals(replica1, replicas.get(1000L), "Replica on backend 1000 should match");
Assertions.assertEquals(replica2, replicas.get(1001L), "Replica on backend 1001 should match");
Assertions.assertEquals(replica3, replicas.get(1002L), "Replica on backend 1002 should match");
}
}