This is an automated email from the ASF dual-hosted git repository.
deardeng 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 634cbaaee97 [improvement](fe) Presize global cloud tablet route sets
(#66447)
634cbaaee97 is described below
commit 634cbaaee977f66e4d0860db2376a0f879093e6b
Author: deardeng <[email protected]>
AuthorDate: Mon Aug 10 10:49:20 2026 +0800
[improvement](fe) Presize global cloud tablet route sets (#66447)
Related PR: #66378, #66389, #66451
Problem Summary: Rebuilding cloud tablet routes for a
multi-million-tablet FE generated substantial short-lived allocation
from repeated ConcurrentHashMap-backed set growth, stale oversized
capacity hints after catalog shrink, per-call hash varargs and boxing,
ArrayList growth, repeated primary backend ID boxing, replica iterators,
and inflight lookup keys created while the map was empty.
Presize current and future global route sets from the corresponding
previous route cardinality, while bounding each hint at 1,048,576
entries. Compute InfightTablet hashes without varargs or boxing, size
route lists from the index tablet count, reuse the immutable boxed
primary backend ID stored by CloudReplica, traverse replica lists by
index, and skip inflight key construction on the empty-map fast path.
Route contents, scheduling decisions, incremental updates, and
persistence semantics are unchanged.
For 4 million tablets across 4 clusters, a JDK 17 allocation model
estimated that global-set presizing reduced allocation on that path from
1.94 GiB to 1.62 GiB (16.14%). In the sharp-shrink case, bounding two
stale four-million-entry hints reduced modeled allocation from 128.01
MiB to 32.01 MiB (75.00%) and retained heap from 130.00 MiB to 34.00 MiB
(73.85%). Direct hashing and exact list sizing were estimated to remove
78.06 to 82.35 GiB of cumulative allocation per 30 minutes. After the
final boxed-ID, replica-iteration, and empty-inflight-map changes, a
downstream 30-minute JFR comparison measured total FE allocation
decreasing from 751.49 GiB to 566.14 GiB (24.7%) and rebalancer-thread
allocation decreasing from 670.34 GiB to 484.52 GiB (185.82 GiB, 27.7%),
with all three targeted allocation stacks reduced to zero. Model figures
are path estimates; JFR figures are cumulative allocations for the test
workload, not production RSS measurements.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- `./run-fe-ut.sh --run
org.apache.doris.cloud.catalog.CloudTabletRebalancerTest` (17 tests
passed)
- `mvn checkstyle:check -pl fe-core` (0 violations)
- Single-threaded JDK 17 multi-scale allocation model
- Behavior changed: No
- Does this need documentation: No
---
.../apache/doris/cloud/catalog/CloudReplica.java | 4 +
.../doris/cloud/catalog/CloudTabletRebalancer.java | 134 +++++--
.../org/apache/doris/system/SystemInfoService.java | 4 +
.../cloud/catalog/CloudTabletRebalancerTest.java | 399 ++++++++++++++++++++-
4 files changed, 502 insertions(+), 39 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 e507eb49e8d..caec16887ac 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
@@ -283,6 +283,10 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
return primaryClusterToBackend.getOrDefault(clusterId, -1L);
}
+ Long getNonColocatedPrimaryBackendId(String clusterId) {
+ return primaryClusterToBackend.get(clusterId);
+ }
+
// For proc display only. In cloud mode a replica is hashed to a different
BE in each
// compute group, so expose a clusterId -> backendId mapping; the proc
display builds
// a separate bucket sequence per compute group from it so each group's
sequence is
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java
index b8a6f833d7a..e65626e0f88 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudTabletRebalancer.java
@@ -52,6 +52,7 @@ import org.apache.doris.thrift.TStatusCode;
import org.apache.doris.thrift.TWarmUpCacheAsyncRequest;
import org.apache.doris.thrift.TWarmUpCacheAsyncResponse;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
@@ -77,10 +78,14 @@ import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
import java.util.stream.Collectors;
public class CloudTabletRebalancer extends MasterDaemon {
private static final Logger LOG =
LogManager.getLogger(CloudTabletRebalancer.class);
+ private static final int MAX_GLOBAL_TABLET_SET_INITIAL_CAPACITY = 1 << 16;
+ private static final Function<Long, Set<Long>>
DEFAULT_GLOBAL_TABLET_SET_FACTORY =
+ ignored -> ConcurrentHashMap.newKeySet();
private final CloudTabletRebalancerMetrics rebalancerMetrics;
private long currentRoundTabletScanCount;
@@ -320,7 +325,7 @@ public class CloudTabletRebalancer extends MasterDaemon {
}
@Getter
- private class InfightTablet {
+ private static class InfightTablet {
private final long tabletId;
private final String clusterId;
@@ -343,7 +348,9 @@ public class CloudTabletRebalancer extends MasterDaemon {
@Override
public int hashCode() {
- return Objects.hash(tabletId, clusterId);
+ int result = 1;
+ result = 31 * result + Long.hashCode(tabletId);
+ return 31 * result + clusterId.hashCode();
}
}
@@ -445,39 +452,50 @@ public class CloudTabletRebalancer extends MasterDaemon {
}
public Set<Long> getSnapshotTabletsInPrimaryByBeId(Long beId) {
- Set<Long> tabletIds = Sets.newHashSet();
Set<Long> tablets = beToTabletsGlobal.get(beId);
- if (tablets != null) {
- // Create a copy
- tabletIds.addAll(new HashSet<>(tablets));
- }
-
Set<Long> colocateTablets = beToColocateTabletsGlobal.get(beId);
- if (colocateTablets != null) {
- // Create a copy
- tabletIds.addAll(new HashSet<>(colocateTablets));
- }
+ Set<Long> tabletIds = newSnapshotTabletSet(tabletSetSize(tablets) +
tabletSetSize(colocateTablets));
+ addSnapshotTablets(tabletIds, tablets);
+ addSnapshotTablets(tabletIds, colocateTablets);
return tabletIds;
}
public Set<Long> getSnapshotTabletsInSecondaryByBeId(Long beId) {
- Set<Long> tabletIds = Sets.newHashSet();
Set<Long> tablets = beToTabletsGlobalInSecondary.get(beId);
- if (tablets != null) {
- // Create a copy
- tabletIds.addAll(new HashSet<>(tablets));
- }
+ Set<Long> tabletIds = newSnapshotTabletSet(tabletSetSize(tablets));
+ addSnapshotTablets(tabletIds, tablets);
return tabletIds;
}
public Set<Long> getSnapshotTabletsInPrimaryAndSecondaryByBeId(Long beId) {
- Set<Long> tabletIds = Sets.newHashSet();
- tabletIds.addAll(getSnapshotTabletsInPrimaryByBeId(beId));
- tabletIds.addAll(getSnapshotTabletsInSecondaryByBeId(beId));
+ Set<Long> primaryTablets = beToTabletsGlobal.get(beId);
+ Set<Long> colocateTablets = beToColocateTabletsGlobal.get(beId);
+ Set<Long> secondaryTablets = beToTabletsGlobalInSecondary.get(beId);
+ int expectedSize = tabletSetSize(primaryTablets)
+ + tabletSetSize(colocateTablets) +
tabletSetSize(secondaryTablets);
+ Set<Long> tabletIds = newSnapshotTabletSet(expectedSize);
+ addSnapshotTablets(tabletIds, primaryTablets);
+ addSnapshotTablets(tabletIds, colocateTablets);
+ addSnapshotTablets(tabletIds, secondaryTablets);
return tabletIds;
}
+ private static int tabletSetSize(Set<Long> tablets) {
+ return tablets == null ? 0 : tablets.size();
+ }
+
+ private static void addSnapshotTablets(Set<Long> snapshot, Set<Long>
tablets) {
+ if (tablets != null) {
+ snapshot.addAll(tablets);
+ }
+ }
+
+ @VisibleForTesting
+ protected Set<Long> newSnapshotTabletSet(int expectedSize) {
+ return Sets.newHashSetWithExpectedSize(expectedSize);
+ }
+
public int getTabletNumByBackendId(long beId) {
Map<Long, Set<Long>> sourceMap = beToTabletsGlobal;
ConcurrentHashMap<Long, Set<Long>> futureMap = futureBeToTabletsGlobal;
@@ -979,34 +997,38 @@ public class CloudTabletRebalancer extends MasterDaemon {
long needRehashDeadTime = System.currentTimeMillis() -
Config.rehash_tablet_after_be_dead_seconds * 1000L;
loopCloudReplica((Database db, Table table, Partition partition,
MaterializedIndex index, String cluster) -> {
boolean assigned = false;
- List<Long> beIds = new ArrayList<Long>();
- List<Long> tabletIds = new ArrayList<Long>();
+ List<Tablet> tablets = index.getTablets();
boolean isColocated =
Env.getCurrentColocateIndex().isColocateTable(table.getId());
- for (Tablet tablet : index.getTablets()) {
+ int routeCount = isColocated ? 0 : tablets.size();
+ List<Long> beIds = newRouteInfoList(routeCount);
+ List<Long> tabletIds = newRouteInfoList(routeCount);
+ for (Tablet tablet : tablets) {
for (Replica r : tablet.getReplicas()) {
CloudReplica replica = (CloudReplica) r;
// clean secondary map
replica.checkAndClearSecondaryClusterToBe(cluster,
needRehashDeadTime);
- InfightTablet taskKey = new InfightTablet(tablet.getId(),
cluster);
// colocate table no need to update primary backends
if (isColocated) {
replica.clearClusterToBe(cluster);
- tabletToInfightTask.remove(taskKey);
+ tabletToInfightTask.remove(new
InfightTablet(tablet.getId(), cluster));
continue;
}
// primary backend is alive or dead not long
- Backend be = replica.getPrimaryBackend(cluster, false);
+ Long primaryBeId =
replica.getNonColocatedPrimaryBackendId(cluster);
+ Backend be = primaryBeId == null
+ ? null :
Env.getCurrentSystemInfo().getBackendByIdWithBoxedId(primaryBeId);
if (be != null && (be.isQueryAvailable()
|| (!be.isQueryDisabled()
// Compatible with older version upgrades, see
https://github.com/apache/doris/pull/42986
&& (be.getLastUpdateMs() <= 0 ||
be.getLastUpdateMs() > needRehashDeadTime)))) {
- beIds.add(be.getId());
+ beIds.add(primaryBeId);
tabletIds.add(tablet.getId());
continue;
}
// primary backend not available too long, change one
+ InfightTablet taskKey = new InfightTablet(tablet.getId(),
cluster);
long beId = -1L;
be = replica.getSecondaryBackend(cluster);
if (be != null && be.isQueryAvailable()) {
@@ -1068,6 +1090,11 @@ public class CloudTabletRebalancer extends MasterDaemon {
return true;
}
+ @VisibleForTesting
+ protected <T> List<T> newRouteInfoList(int initialCapacity) {
+ return new ArrayList<>(initialCapacity);
+ }
+
public void fillBeToTablets(long be, long tableId, long partId, long
indexId, long tabletId,
ConcurrentHashMap<Long, Set<Long>>
globalBeToTablets,
ConcurrentHashMap<Long,
ConcurrentHashMap<Long, Set<Long>>> beToTabletsInTable,
@@ -1082,8 +1109,18 @@ public class CloudTabletRebalancer extends MasterDaemon {
ConcurrentHashMap<Long,
ConcurrentHashMap<Long, Set<Long>>> beToTabletsInTable,
ConcurrentHashMap<Long,
ConcurrentHashMap<Long, ConcurrentHashMap<Long, Set<Long>>>>
partToTablets) {
+ fillBeToTablets(be, tableId, partId, indexId, tabletId,
DEFAULT_GLOBAL_TABLET_SET_FACTORY,
+ globalBeToTablets, beToTabletsInTable, partToTablets);
+ }
+
+ private void fillBeToTablets(Long be, Long tableId, Long partId, Long
indexId, Long tabletId,
+ Function<Long, Set<Long>>
globalTabletSetFactory,
+ ConcurrentHashMap<Long, Set<Long>>
globalBeToTablets,
+ ConcurrentHashMap<Long,
ConcurrentHashMap<Long, Set<Long>>> beToTabletsInTable,
+ ConcurrentHashMap<Long,
ConcurrentHashMap<Long, ConcurrentHashMap<Long, Set<Long>>>>
+ partToTablets) {
// global
- globalBeToTablets.computeIfAbsent(be, ignored ->
ConcurrentHashMap.newKeySet()).add(tabletId);
+ globalBeToTablets.computeIfAbsent(be,
globalTabletSetFactory).add(tabletId);
// table
ConcurrentHashMap<Long, Set<Long>> beToTabletsOfTable =
@@ -1098,6 +1135,23 @@ public class CloudTabletRebalancer extends MasterDaemon {
beToTabletsOfIndex.computeIfAbsent(be, ignored ->
ConcurrentHashMap.newKeySet()).add(tabletId);
}
+ private Function<Long, Set<Long>> newGlobalTabletSetFactory(Map<Long,
Set<Long>> previousBeToTablets) {
+ Map<Long, Set<Long>> previousRoute = previousBeToTablets == null
+ ? Collections.emptyMap() : previousBeToTablets;
+ return be -> {
+ Set<Long> previousTablets = previousRoute.get(be);
+ int initialCapacity = previousTablets == null ? 0
+ : Math.min(previousTablets.size(),
MAX_GLOBAL_TABLET_SET_INITIAL_CAPACITY);
+ return newGlobalTabletSet(initialCapacity);
+ };
+ }
+
+ @VisibleForTesting
+ protected Set<Long> newGlobalTabletSet(int initialCapacity) {
+ return initialCapacity == 0
+ ? ConcurrentHashMap.newKeySet() :
ConcurrentHashMap.newKeySet(initialCapacity);
+ }
+
private void enqueueWarmupTask(WarmupTabletTask task) {
WarmupBatchKey key = new WarmupBatchKey(task.srcBe, task.destBe);
WarmupBatch batch = warmupBatches.computeIfAbsent(key,
WarmupBatch::new);
@@ -1173,6 +1227,12 @@ public class CloudTabletRebalancer extends MasterDaemon {
}
public void statRouteInfo() {
+ // The previous generation remains live until the temporary global
routes are complete, so reuse its
+ // per-backend cardinalities as allocation hints without extending its
lifetime.
+ Function<Long, Set<Long>> currentGlobalTabletSetFactory =
+ newGlobalTabletSetFactory(beToTabletsGlobal);
+ Function<Long, Set<Long>> futureGlobalTabletSetFactory =
+ newGlobalTabletSetFactory(futureBeToTabletsGlobal);
ConcurrentHashMap<Long, Set<Long>> tmpBeToTabletsGlobal = new
ConcurrentHashMap<Long, Set<Long>>();
ConcurrentHashMap<Long, Set<Long>> tmpFutureBeToTabletsGlobal = new
ConcurrentHashMap<Long, Set<Long>>();
ConcurrentHashMap<Long, Set<Long>> tmpBeToTabletsGlobalInSecondary
@@ -1216,8 +1276,10 @@ public class CloudTabletRebalancer extends MasterDaemon {
tmpPartitionActive.merge(partitionId, 1L, Long::sum);
tmpDbActive.merge(dbId, 1L, Long::sum);
}
- for (Replica r : tablet.getReplicas()) {
- CloudReplica replica = (CloudReplica) r;
+ List<Replica> replicas = tablet.getReplicas();
+ int replicaCount = replicas.size();
+ for (int replicaIndex = 0; replicaIndex < replicaCount;
replicaIndex++) {
+ CloudReplica replica = (CloudReplica)
replicas.get(replicaIndex);
if (isColocated) {
Long beId = -1L;
try {
@@ -1233,8 +1295,10 @@ public class CloudTabletRebalancer extends MasterDaemon {
continue;
}
- Backend be = replica.getPrimaryBackend(cluster, false);
- Long beId = be == null ? Long.valueOf(-1L) :
Long.valueOf(be.getId());
+ Long primaryBeId =
replica.getNonColocatedPrimaryBackendId(cluster);
+ Backend be = primaryBeId == null
+ ? null :
Env.getCurrentSystemInfo().getBackendByIdWithBoxedId(primaryBeId);
+ Long beId = be == null ? Long.valueOf(-1L) : primaryBeId;
if (!allBes.contains(beId)) {
continue;
}
@@ -1247,14 +1311,16 @@ public class CloudTabletRebalancer extends MasterDaemon
{
tablets.add(tabletId);
}
- InfightTablet taskKey = new InfightTablet(tabletId,
cluster);
- InfightTask task = tabletToInfightTask.get(taskKey);
+ InfightTask task = tabletToInfightTask.isEmpty() ? null
+ : tabletToInfightTask.get(new
InfightTablet(tabletId, cluster));
Long futureBeId = task == null ? beId :
Long.valueOf(task.destBe);
Long routeTabletId = task == null ? tabletId :
task.pickedTabletId;
fillBeToTablets(beId, tableId, partitionId, indexId,
routeTabletId,
+ currentGlobalTabletSetFactory,
tmpBeToTabletsGlobal, beToTabletsInTable,
this.partitionToTablets);
fillBeToTablets(futureBeId, tableId, partitionId, indexId,
routeTabletId,
+ futureGlobalTabletSetFactory,
tmpFutureBeToTabletsGlobal,
futureBeToTabletsInTable, futurePartitionToTablets);
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/system/SystemInfoService.java
b/fe/fe-core/src/main/java/org/apache/doris/system/SystemInfoService.java
index 8b5a80a978e..5713be5b965 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/system/SystemInfoService.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/system/SystemInfoService.java
@@ -335,6 +335,10 @@ public class SystemInfoService {
return getAllClusterBackendsNoException().get(backendId);
}
+ public Backend getBackendByIdWithBoxedId(Long backendId) {
+ return getAllClusterBackendsNoException().get(backendId);
+ }
+
public List<Backend> getBackends(List<Long> backendIds) {
List<Backend> backends = Lists.newArrayList();
for (long backendId : backendIds) {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudTabletRebalancerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudTabletRebalancerTest.java
index b56f4e4a463..f183a28cfcc 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudTabletRebalancerTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudTabletRebalancerTest.java
@@ -23,6 +23,7 @@ import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.MaterializedIndex;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.Partition;
+import org.apache.doris.catalog.Replica;
import org.apache.doris.catalog.Tablet;
import org.apache.doris.catalog.TabletInvertedIndex;
import org.apache.doris.catalog.TabletMeta;
@@ -32,6 +33,7 @@ import org.apache.doris.common.Config;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.metric.MetricRepo;
import org.apache.doris.system.Backend;
+import org.apache.doris.system.SystemInfoService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
@@ -40,8 +42,11 @@ import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
@@ -97,6 +102,33 @@ public class CloudTabletRebalancerTest {
}
}
+ private static class CapacityTrackingRebalancer extends TestRebalancer {
+ private final List<Integer> globalTabletSetInitialCapacities = new
ArrayList<>();
+ private final List<Integer> routeInfoListInitialCapacities = new
ArrayList<>();
+
+ @Override
+ protected Set<Long> newGlobalTabletSet(int initialCapacity) {
+ globalTabletSetInitialCapacities.add(initialCapacity);
+ return ConcurrentHashMap.newKeySet();
+ }
+
+ @Override
+ protected <T> List<T> newRouteInfoList(int initialCapacity) {
+ routeInfoListInitialCapacities.add(initialCapacity);
+ return super.newRouteInfoList(initialCapacity);
+ }
+ }
+
+ private static class SnapshotCapacityTrackingRebalancer extends
TestRebalancer {
+ private final List<Integer> snapshotTabletSetInitialCapacities = new
ArrayList<>();
+
+ @Override
+ protected Set<Long> newSnapshotTabletSet(int expectedSize) {
+ snapshotTabletSetInitialCapacities.add(expectedSize);
+ return super.newSnapshotTabletSet(expectedSize);
+ }
+ }
+
private static class CountingConcurrentHashMap<K, V> extends
ConcurrentHashMap<K, V> {
private int computeIfAbsentCalls;
private int getCalls;
@@ -121,6 +153,39 @@ public class CloudTabletRebalancerTest {
}
}
+ private static class IteratorRejectingList<E> extends AbstractList<E> {
+ private final E element;
+
+ IteratorRejectingList(E element) {
+ this.element = element;
+ }
+
+ @Override
+ public E get(int index) {
+ if (index != 0) {
+ throw new IndexOutOfBoundsException(String.valueOf(index));
+ }
+ return element;
+ }
+
+ @Override
+ public int size() {
+ return 1;
+ }
+
+ @Override
+ public java.util.Iterator<E> iterator() {
+ throw new AssertionError("replica traversal must not allocate an
iterator");
+ }
+ }
+
+ private static class EmptyLookupRejectingMap<K, V> extends
ConcurrentHashMap<K, V> {
+ @Override
+ public V get(Object key) {
+ throw new AssertionError("an empty inflight map must not allocate
and probe a composite key");
+ }
+ }
+
private static void setField(Object obj, String name, Object value) throws
Exception {
Field f = CloudTabletRebalancer.class.getDeclaredField(name);
f.setAccessible(true);
@@ -164,6 +229,207 @@ public class CloudTabletRebalancerTest {
byPartition = new ConcurrentHashMap<>();
}
+ @Test
+ public void testInfightTabletIsStaticAndPreservesHashCode() throws
Exception {
+ long tabletId = 50_001L;
+ String clusterId = "cluster-a";
+ Class<?> infightTabletClass = null;
+ for (Class<?> nestedClass :
CloudTabletRebalancer.class.getDeclaredClasses()) {
+ if (nestedClass.getSimpleName().equals("InfightTablet")) {
+ infightTabletClass = nestedClass;
+ break;
+ }
+ }
+ Assertions.assertNotNull(infightTabletClass);
+
Assertions.assertTrue(Modifier.isStatic(infightTabletClass.getModifiers()));
+ Constructor<?> constructor =
infightTabletClass.getDeclaredConstructor(long.class, String.class);
+ constructor.setAccessible(true);
+ Object infightTablet = constructor.newInstance(tabletId, clusterId);
+
+ int expectedHashCode = 31 * (31 + Long.hashCode(tabletId)) +
clusterId.hashCode();
+ Assertions.assertEquals(expectedHashCode, infightTablet.hashCode());
+ }
+
+ @Test
+ public void testCloudReplicaReturnsStoredBoxedPrimaryBackendId() throws
Exception {
+ CloudReplica replica = new CloudReplica();
+ String clusterId = "cluster-a";
+ long backendId = 60_001L;
+ replica.updateClusterToPrimaryBe(clusterId, backendId);
+
+ Method method = CloudReplica.class.getDeclaredMethod(
+ "getNonColocatedPrimaryBackendId", String.class);
+ method.setAccessible(true);
+ Long first = (Long) method.invoke(replica, clusterId);
+ Long second = (Long) method.invoke(replica, clusterId);
+
+ Assertions.assertEquals(backendId, first);
+ Assertions.assertSame(first, second);
+ }
+
+ @Test
+ public void testSystemInfoServiceLooksUpBackendWithBoxedId() throws
Exception {
+ SystemInfoService systemInfoService = new SystemInfoService();
+ Long backendId = Long.valueOf(60_001L);
+ Backend backend = new Backend(backendId, "127.0.0.1", 9050);
+ systemInfoService.addBackend(backend);
+
+ Method method =
SystemInfoService.class.getDeclaredMethod("getBackendByIdWithBoxedId",
Long.class);
+ Backend actual = (Backend) method.invoke(systemInfoService, backendId);
+
+ Assertions.assertSame(backend, actual);
+ }
+
+ @Test
+ public void testRouteRebuildDoesNotUsePrimitivePrimaryBackendPath() throws
Exception {
+ TestRebalancer rebalancer = new TestRebalancer();
+ Long dbId = 10_001L;
+ Long tableId = 20_001L;
+ Long partitionId = 30_001L;
+ Long indexId = 40_001L;
+ Long tabletId = 50_001L;
+ Long beId = 60_001L;
+ String clusterId = "cluster-a";
+ Tablet tablet = mockTablet(tabletId);
+ CloudReplica replica = (CloudReplica) tablet.getReplicas().get(0);
+ setField(rebalancer, "clusterToBes",
Collections.singletonMap(clusterId, List.of(beId)));
+ setField(rebalancer, "allBes", Set.of(beId));
+
+ try (MockedStatic<Env> ignored = mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tablet, clusterId, beId))
{
+ boolean completed = invokePrivate(rebalancer, "completeRouteInfo",
+ new Class<?>[] {}, new Object[] {});
+ Assertions.assertTrue(completed);
+ rebalancer.statRouteInfo();
+ }
+
+ Mockito.verify(replica, Mockito.never()).getPrimaryBackend(clusterId,
false);
+ }
+
+ @Test
+ public void testStatRouteInfoTraversesReplicasWithoutIterator() throws
Exception {
+ TestRebalancer rebalancer = new TestRebalancer();
+ Long dbId = 10_001L;
+ Long tableId = 20_001L;
+ Long partitionId = 30_001L;
+ Long indexId = 40_001L;
+ Long tabletId = 50_001L;
+ Long beId = 60_001L;
+ String clusterId = "cluster-a";
+ Tablet tablet = mockTablet(tabletId);
+ CloudReplica replica = (CloudReplica) tablet.getReplicas().get(0);
+ Mockito.when(tablet.getReplicas()).thenReturn(new
IteratorRejectingList<Replica>(replica));
+ setField(rebalancer, "clusterToBes",
Collections.singletonMap(clusterId, List.of(beId)));
+ setField(rebalancer, "allBes", Set.of(beId));
+
+ try (MockedStatic<Env> ignored = mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tablet, clusterId, beId))
{
+ rebalancer.statRouteInfo();
+ }
+ }
+
+ @Test
+ public void testStatRouteInfoSkipsCompositeKeyForEmptyInflightMap() throws
Exception {
+ TestRebalancer rebalancer = new TestRebalancer();
+ Long dbId = 10_001L;
+ Long tableId = 20_001L;
+ Long partitionId = 30_001L;
+ Long indexId = 40_001L;
+ Long tabletId = 50_001L;
+ Long beId = 60_001L;
+ String clusterId = "cluster-a";
+ Tablet tablet = mockTablet(tabletId);
+ setField(rebalancer, "clusterToBes",
Collections.singletonMap(clusterId, List.of(beId)));
+ setField(rebalancer, "allBes", Set.of(beId));
+ setField(rebalancer, "tabletToInfightTask", new
EmptyLookupRejectingMap<>());
+
+ try (MockedStatic<Env> ignored = mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tablet, clusterId, beId))
{
+ rebalancer.statRouteInfo();
+ }
+ }
+
+ @Test
+ public void testCompleteRouteInfoPresizesRouteListsFromIndexTablets()
throws Exception {
+ CapacityTrackingRebalancer rebalancer = new
CapacityTrackingRebalancer();
+ Long dbId = 10_001L;
+ Long tableId = 20_001L;
+ Long partitionId = 30_001L;
+ Long indexId = 40_001L;
+ Long tabletId = 50_001L;
+ Long beId = 60_001L;
+ String clusterId = "cluster-a";
+ setField(rebalancer, "clusterToBes",
Collections.singletonMap(clusterId, List.of(beId)));
+
+ try (MockedStatic<Env> ignored = mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tabletId, clusterId,
beId, 3)) {
+ boolean completed = invokePrivate(rebalancer, "completeRouteInfo",
+ new Class<?>[] {}, new Object[] {});
+
+ Assertions.assertTrue(completed);
+ Assertions.assertEquals(List.of(3, 3),
rebalancer.routeInfoListInitialCapacities);
+ }
+ }
+
+ @Test
+ public void testCompleteRouteInfoDoesNotPresizeUnusedColocateRouteLists()
throws Exception {
+ CapacityTrackingRebalancer rebalancer = new
CapacityTrackingRebalancer();
+ Long dbId = 10_001L;
+ Long tableId = 20_001L;
+ Long partitionId = 30_001L;
+ Long indexId = 40_001L;
+ Long tabletId = 50_001L;
+ Long beId = 60_001L;
+ String clusterId = "cluster-a";
+ setField(rebalancer, "clusterToBes",
Collections.singletonMap(clusterId, List.of(beId)));
+
+ try (MockedStatic<Env> ignored = mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tabletId, clusterId,
beId, 3, true)) {
+ boolean completed = invokePrivate(rebalancer, "completeRouteInfo",
+ new Class<?>[] {}, new Object[] {});
+
+ Assertions.assertTrue(completed);
+ Assertions.assertEquals(List.of(0, 0),
rebalancer.routeInfoListInitialCapacities);
+ }
+ }
+
+ @Test
+ public void testSnapshotTabletSetsUseOnePresizedResultPerRequest() throws
Exception {
+ SnapshotCapacityTrackingRebalancer rebalancer = new
SnapshotCapacityTrackingRebalancer();
+ Long beId = 60_001L;
+ ConcurrentHashMap<Long, Set<Long>> primary = new ConcurrentHashMap<>();
+ primary.put(beId, Set.of(50_001L, 50_002L));
+ ConcurrentHashMap<Long, Set<Long>> colocate = new
ConcurrentHashMap<>();
+ colocate.put(beId, Set.of(50_002L, 50_003L));
+ ConcurrentHashMap<Long, Set<Long>> secondary = new
ConcurrentHashMap<>();
+ secondary.put(beId, Set.of(50_003L, 50_004L));
+ setField(rebalancer, "beToTabletsGlobal", primary);
+ setField(rebalancer, "beToColocateTabletsGlobal", colocate);
+ setField(rebalancer, "beToTabletsGlobalInSecondary", secondary);
+
+ Assertions.assertEquals(Set.of(50_001L, 50_002L, 50_003L),
+ rebalancer.getSnapshotTabletsInPrimaryByBeId(beId));
+ Assertions.assertEquals(Set.of(50_003L, 50_004L),
+ rebalancer.getSnapshotTabletsInSecondaryByBeId(beId));
+ Assertions.assertEquals(Set.of(50_001L, 50_002L, 50_003L, 50_004L),
+
rebalancer.getSnapshotTabletsInPrimaryAndSecondaryByBeId(beId));
+
+ Assertions.assertEquals(List.of(4, 2, 6),
+ rebalancer.snapshotTabletSetInitialCapacities);
+ }
+
+ @Test
+ public void testSnapshotTabletSetsRemainEmptyForUnknownBackend() {
+ SnapshotCapacityTrackingRebalancer rebalancer = new
SnapshotCapacityTrackingRebalancer();
+ Long unknownBeId = 60_001L;
+
+
Assertions.assertTrue(rebalancer.getSnapshotTabletsInPrimaryByBeId(unknownBeId).isEmpty());
+
Assertions.assertTrue(rebalancer.getSnapshotTabletsInSecondaryByBeId(unknownBeId).isEmpty());
+
Assertions.assertTrue(rebalancer.getSnapshotTabletsInPrimaryAndSecondaryByBeId(unknownBeId).isEmpty());
+ Assertions.assertEquals(List.of(0, 0, 0),
+ rebalancer.snapshotTabletSetInitialCapacities);
+ }
+
@Test
public void testFillBeToTabletsReusesBoxedIdsAcrossIndexes() {
TestRebalancer rebalancer = new TestRebalancer();
@@ -349,6 +615,96 @@ public class CloudTabletRebalancerTest {
}
}
+ @Test
+ public void testStatRouteInfoPresizesGlobalTabletSetsFromPreviousRoute()
throws Exception {
+ CapacityTrackingRebalancer rebalancer = new
CapacityTrackingRebalancer();
+ Long dbId = 10_001L;
+ Long tableId = 20_001L;
+ Long partitionId = 30_001L;
+ Long indexId = 40_001L;
+ Long tabletId = 50_001L;
+ Long beId = 60_001L;
+ String clusterId = "cluster-a";
+
+ ConcurrentHashMap<Long, Set<Long>> previousCurrent = new
ConcurrentHashMap<>();
+ previousCurrent.put(beId, Set.of(1L, 2L, 3L));
+ ConcurrentHashMap<Long, Set<Long>> previousFuture = new
ConcurrentHashMap<>();
+ previousFuture.put(beId, Set.of(1L, 2L, 3L, 4L, 5L));
+ setField(rebalancer, "beToTabletsGlobal", previousCurrent);
+ setField(rebalancer, "futureBeToTabletsGlobal", previousFuture);
+ setField(rebalancer, "clusterToBes",
Collections.singletonMap(clusterId, List.of(beId)));
+ setField(rebalancer, "allBes", Set.of(beId));
+
+ try (MockedStatic<Env> ignored = mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tabletId, clusterId,
beId)) {
+ rebalancer.statRouteInfo();
+ }
+
+ Assertions.assertEquals(List.of(3, 5),
rebalancer.globalTabletSetInitialCapacities);
+ ConcurrentHashMap<Long, Set<Long>> current = getField(rebalancer,
"beToTabletsGlobal");
+ ConcurrentHashMap<Long, Set<Long>> future = getField(rebalancer,
"futureBeToTabletsGlobal");
+ Assertions.assertEquals(Set.of(tabletId), current.get(beId));
+ Assertions.assertEquals(Set.of(tabletId), future.get(beId));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testStatRouteInfoBoundsStaleGlobalTabletSetCapacity() throws
Exception {
+ CapacityTrackingRebalancer rebalancer = new
CapacityTrackingRebalancer();
+ Long dbId = 10_001L;
+ Long tableId = 20_001L;
+ Long partitionId = 30_001L;
+ Long indexId = 40_001L;
+ Long tabletId = 50_001L;
+ Long beId = 60_001L;
+ String clusterId = "cluster-a";
+
+ Set<Long> stalePreviousTablets = Mockito.mock(Set.class);
+ Mockito.when(stalePreviousTablets.size()).thenReturn(2_000_000);
+ ConcurrentHashMap<Long, Set<Long>> previousCurrent = new
ConcurrentHashMap<>();
+ previousCurrent.put(beId, stalePreviousTablets);
+ ConcurrentHashMap<Long, Set<Long>> previousFuture = new
ConcurrentHashMap<>();
+ previousFuture.put(beId, stalePreviousTablets);
+ setField(rebalancer, "beToTabletsGlobal", previousCurrent);
+ setField(rebalancer, "futureBeToTabletsGlobal", previousFuture);
+ setField(rebalancer, "clusterToBes",
Collections.singletonMap(clusterId, List.of(beId)));
+ setField(rebalancer, "allBes", Set.of(beId));
+
+ try (MockedStatic<Env> ignored = mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tabletId, clusterId,
beId)) {
+ rebalancer.statRouteInfo();
+ }
+
+ Assertions.assertEquals(List.of(65_536, 65_536),
+ rebalancer.globalTabletSetInitialCapacities);
+ ConcurrentHashMap<Long, Set<Long>> current = getField(rebalancer,
"beToTabletsGlobal");
+ ConcurrentHashMap<Long, Set<Long>> future = getField(rebalancer,
"futureBeToTabletsGlobal");
+ Assertions.assertEquals(Set.of(tabletId), current.get(beId));
+ Assertions.assertEquals(Set.of(tabletId), future.get(beId));
+ }
+
+ @Test
+ public void testStatRouteInfoUsesZeroCapacityForNewBackend() throws
Exception {
+ CapacityTrackingRebalancer rebalancer = new
CapacityTrackingRebalancer();
+ Long dbId = 10_001L;
+ Long tableId = 20_001L;
+ Long partitionId = 30_001L;
+ Long indexId = 40_001L;
+ Long tabletId = 50_001L;
+ Long beId = 60_001L;
+ String clusterId = "cluster-a";
+
+ setField(rebalancer, "clusterToBes",
Collections.singletonMap(clusterId, List.of(beId)));
+ setField(rebalancer, "allBes", Set.of(beId));
+
+ try (MockedStatic<Env> ignored = mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tabletId, clusterId,
beId)) {
+ rebalancer.statRouteInfo();
+ }
+
+ Assertions.assertEquals(List.of(0, 0),
rebalancer.globalTabletSetInitialCapacities);
+ }
+
private static void initializeRouteMaps(TestRebalancer rebalancer,
RouteMaps current, RouteMaps future,
Long srcBe, Long tableId, Long partitionId, Long indexId, Long
tabletId) throws Exception {
rebalancer.fillBeToTablets(srcBe, tableId, partitionId, indexId,
tabletId,
@@ -381,6 +737,36 @@ public class CloudTabletRebalancerTest {
private static MockedStatic<Env> mockRouteEnvironment(Long dbId, Long
tableId, Long partitionId,
Long indexId, Long tabletId, String clusterId, Long srcBe) {
+ return mockRouteEnvironment(dbId, tableId, partitionId, indexId,
tabletId, clusterId, srcBe, 1, false);
+ }
+
+ private static MockedStatic<Env> mockRouteEnvironment(Long dbId, Long
tableId, Long partitionId,
+ Long indexId, Long tabletId, String clusterId, Long srcBe, int
tabletCount) {
+ return mockRouteEnvironment(
+ dbId, tableId, partitionId, indexId, tabletId, clusterId,
srcBe, tabletCount, false);
+ }
+
+ private static MockedStatic<Env> mockRouteEnvironment(Long dbId, Long
tableId, Long partitionId,
+ Long indexId, Long tabletId, String clusterId, Long srcBe, int
tabletCount, boolean colocated) {
+ return mockRouteEnvironment(dbId, tableId, partitionId, indexId,
mockTablet(tabletId),
+ clusterId, srcBe, tabletCount, colocated);
+ }
+
+ private static Tablet mockTablet(long tabletId) {
+ Tablet tablet = Mockito.mock(Tablet.class);
+ CloudReplica replica = Mockito.mock(CloudReplica.class);
+ Mockito.when(tablet.getId()).thenReturn(tabletId);
+
Mockito.when(tablet.getReplicas()).thenReturn(Collections.singletonList(replica));
+ return tablet;
+ }
+
+ private static MockedStatic<Env> mockRouteEnvironment(Long dbId, Long
tableId, Long partitionId,
+ Long indexId, Tablet tablet, String clusterId, Long srcBe) {
+ return mockRouteEnvironment(dbId, tableId, partitionId, indexId,
tablet, clusterId, srcBe, 1, false);
+ }
+
+ private static MockedStatic<Env> mockRouteEnvironment(Long dbId, Long
tableId, Long partitionId,
+ Long indexId, Tablet tablet, String clusterId, Long srcBe, int
tabletCount, boolean colocated) {
Env env = Mockito.mock(Env.class);
TabletInvertedIndex invertedIndex =
Mockito.mock(TabletInvertedIndex.class);
TabletMeta tabletMeta = Mockito.mock(TabletMeta.class);
@@ -390,9 +776,10 @@ public class CloudTabletRebalancerTest {
OlapTable table = Mockito.mock(OlapTable.class);
Partition partition = Mockito.mock(Partition.class);
MaterializedIndex index = Mockito.mock(MaterializedIndex.class);
- Tablet tablet = Mockito.mock(Tablet.class);
- CloudReplica replica = Mockito.mock(CloudReplica.class);
+ CloudReplica replica = (CloudReplica) tablet.getReplicas().get(0);
Backend primaryBackend = Mockito.mock(Backend.class);
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+ Long tabletId = tablet.getId();
Mockito.when(env.getTabletInvertedIndex()).thenReturn(invertedIndex);
Mockito.when(invertedIndex.getTabletMeta(tabletId)).thenReturn(tabletMeta);
@@ -405,6 +792,7 @@ public class CloudTabletRebalancerTest {
Mockito.when(database.getId()).thenReturn(dbId);
Mockito.when(table.isManagedTable()).thenReturn(true);
Mockito.when(table.getId()).thenReturn(tableId);
+
Mockito.when(colocateTableIndex.isColocateTable(tableId)).thenReturn(colocated);
Mockito.when(table.getAllPartitions()).thenReturn(Collections.singletonList(partition));
Mockito.when(partition.getId()).thenReturn(partitionId);
Mockito.when(partition.getMaterializedIndices(MaterializedIndex.IndexExtState.VISIBLE))
@@ -412,9 +800,9 @@ public class CloudTabletRebalancerTest {
Mockito.when(partition.getMaterializedIndices(MaterializedIndex.IndexExtState.VISIBLE,
true))
.thenReturn(Collections.singletonList(index));
Mockito.when(index.getId()).thenReturn(indexId);
-
Mockito.when(index.getTablets()).thenReturn(Collections.singletonList(tablet));
- Mockito.when(tablet.getId()).thenReturn(tabletId);
-
Mockito.when(tablet.getReplicas()).thenReturn(Collections.singletonList(replica));
+
Mockito.when(index.getTablets()).thenReturn(Collections.nCopies(tabletCount,
tablet));
+
Mockito.when(replica.getNonColocatedPrimaryBackendId(clusterId)).thenReturn(srcBe);
+
Mockito.when(systemInfoService.getBackendByIdWithBoxedId(srcBe)).thenReturn(primaryBackend);
Mockito.when(replica.getPrimaryBackend(clusterId,
false)).thenReturn(primaryBackend);
Mockito.when(primaryBackend.getId()).thenReturn(srcBe);
@@ -422,6 +810,7 @@ public class CloudTabletRebalancerTest {
mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
mockedEnv.when(Env::getCurrentInternalCatalog).thenReturn(catalog);
mockedEnv.when(Env::getCurrentColocateIndex).thenReturn(colocateTableIndex);
+
mockedEnv.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
return mockedEnv;
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]