This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
commit 7b81582c97686c7c4df8fa63dda576123dbe8b39
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Oct 15 09:32:52 2026 +0800
branch-4.1: [fix](cloud) Preserve recreated compute group mapping #67913
(#68939)
Cherry-picked from #67913
---------
Co-authored-by: deardeng <[email protected]>
---
.../doris/cloud/catalog/CloudClusterChecker.java | 25 +-
.../cloud/catalog/CloudInstanceStatusChecker.java | 59 +++-
.../apache/doris/cloud/catalog/CloudReplica.java | 2 +-
.../doris/cloud/system/CloudSystemInfoService.java | 74 +++--
.../cloud/catalog/CloudClusterCheckerTest.java | 308 +++++++++++++++++++++
.../cloud/system/CloudSystemInfoServiceTest.java | 44 +++
.../test_compute_group_name_reconciliation.out | 21 ++
.../test_compute_group_name_reconciliation.groovy | 169 +++++++++++
8 files changed, 651 insertions(+), 51 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudClusterChecker.java
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudClusterChecker.java
index c8eee36f505..c38bc7debc8 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudClusterChecker.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudClusterChecker.java
@@ -27,6 +27,7 @@ import org.apache.doris.cloud.system.CloudSystemInfoService;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.UserException;
+import org.apache.doris.common.util.DebugPointUtil;
import org.apache.doris.common.util.MasterDaemon;
import org.apache.doris.ha.FrontendNodeType;
import org.apache.doris.metric.MetricRepo;
@@ -146,19 +147,22 @@ public class CloudClusterChecker extends MasterDaemon {
if (LOG.isDebugEnabled()) {
LOG.debug("begin to drop clusterId: {}", delId);
}
- String delClusterName =
cloudSystemInfoService.getClusterNameByClusterId(delId);
- if (delClusterName.isEmpty()) {
- return;
- }
- ((CloudEnv)
Env.getCurrentEnv()).getCacheHotspotMgr().cancelTableFilterJobsForClusterChange(
- delClusterName, "system cancel: compute group " +
delClusterName + " dropped");
List<Backend> toDel =
new
ArrayList<>(finalClusterIdToBackend.getOrDefault(delId, new ArrayList<>()));
+ // The name index may already belong to a same-name
replacement. Use the
+ // obsolete group's own BE tags, and never skip ID cleanup for
a missing name.
+ String delClusterName =
toDel.stream().map(Backend::getCloudClusterName).findFirst()
+ .orElseGet(() ->
cloudSystemInfoService.getClusterNameByClusterId(delId));
+ // Name-scoped jobs may already belong to the replacement
group.
+ if
(delId.equals(cloudSystemInfoService.getCloudClusterIdByName(delClusterName))) {
+ ((CloudEnv)
Env.getCurrentEnv()).getCacheHotspotMgr().cancelTableFilterJobsForClusterChange(
+ delClusterName, "system cancel: compute group " +
delClusterName + " dropped");
+ }
cloudSystemInfoService.updateCloudBackends(new ArrayList<>(),
toDel);
// del clusterName
// del clusterID
MetricRepo.unregisterCloudMetrics(delId, delClusterName,
toDel);
- cloudSystemInfoService.dropCluster(delId, delClusterName);
+ cloudSystemInfoService.removeComputeGroup(delId,
delClusterName);
}
);
}
@@ -539,6 +543,11 @@ public class CloudClusterChecker extends MasterDaemon {
}
private void checkCloudBackends() {
+ if
(DebugPointUtil.isEnable("CloudClusterChecker.checkCloudBackends.pause")) {
+ LOG.info("CloudClusterChecker.checkCloudBackends.pause phase={}",
DebugPointUtil.getDebugParamOrDefault(
+ "CloudClusterChecker.checkCloudBackends.pause", "phase",
""));
+ return;
+ }
Map<String, List<Backend>> clusterIdToBackend =
cloudSystemInfoService.getCloudClusterIdToBackend(false);
//rpc to ms, to get mysql user can use cluster_id
// NOTE: rpc args all empty, use cluster_unique_id to get a instance's
all cluster info.
@@ -574,6 +583,8 @@ public class CloudClusterChecker extends MasterDaemon {
// clusterID local == remote, diff nodes
checkDiffNode(remoteClusterIdToPB, clusterIdToBackend);
+
cloudSystemInfoService.refreshComputeGroupNames(remoteClusterIdToPB.values());
+
// check mem map
checkFeNodesMapValid();
} catch (Exception e) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudInstanceStatusChecker.java
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudInstanceStatusChecker.java
index cc9d256d566..72742911daf 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudInstanceStatusChecker.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudInstanceStatusChecker.java
@@ -26,6 +26,7 @@ import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.Pair;
+import org.apache.doris.common.util.DebugPointUtil;
import org.apache.doris.common.util.MasterDaemon;
import org.apache.doris.metric.MetricRepo;
import org.apache.doris.nereids.trees.plans.commands.WarmUpClusterCommand;
@@ -58,6 +59,12 @@ public class CloudInstanceStatusChecker extends MasterDaemon
{
@Override
protected void runAfterCatalogReady() {
try {
+ if
(DebugPointUtil.isEnable("CloudInstanceStatusChecker.runAfterCatalogReady.pause"))
{
+
LOG.info("CloudInstanceStatusChecker.runAfterCatalogReady.pause phase={}",
+ DebugPointUtil.getDebugParamOrDefault(
+
"CloudInstanceStatusChecker.runAfterCatalogReady.pause", "phase", ""));
+ return;
+ }
long start = System.currentTimeMillis();
Cloud.GetInstanceResponse response =
cloudSystemInfoService.getCloudInstance();
if (!isResponseValid(response)) {
@@ -65,6 +72,17 @@ public class CloudInstanceStatusChecker extends MasterDaemon
{
}
Cloud.InstanceInfoPB instance = response.getInstance();
+ if
(DebugPointUtil.isEnable("CloudInstanceStatusChecker.afterGetInstance.pause")
+ && instance.getClustersList().stream().anyMatch(c ->
c.getClusterId().equals(
+ DebugPointUtil.getDebugParamOrDefault(
+
"CloudInstanceStatusChecker.afterGetInstance.pause", "cluster_id", "")))) {
+ LOG.info("CloudInstanceStatusChecker.afterGetInstance.pause
phase={}",
+ DebugPointUtil.getDebugParamOrDefault(
+
"CloudInstanceStatusChecker.afterGetInstance.pause", "phase", ""));
+ while
(DebugPointUtil.isEnable("CloudInstanceStatusChecker.afterGetInstance.pause")) {
+ Thread.sleep(100);
+ }
+ }
cloudSystemInfoService.setInstanceStatus(instance.getStatus());
syncStorageVault(instance);
processVirtualClusters(instance.getClustersList());
@@ -107,8 +125,10 @@ public class CloudInstanceStatusChecker extends
MasterDaemon {
List<Cloud.ClusterPB> computeClusters = new ArrayList<>();
categorizeClusters(clusters, virtualClusters, computeClusters);
handleComputeClusters(computeClusters);
- handleVirtualClusters(virtualClusters, computeClusters);
+ List<Cloud.ClusterPB> reconciledVirtualClusters =
handleVirtualClusters(virtualClusters, computeClusters);
+ // A rejected update still proves that the group exists in MS, so use
the full snapshot for removal.
removeObsoleteVirtualGroups(virtualClusters);
+
cloudSystemInfoService.refreshComputeGroupNames(reconciledVirtualClusters);
}
private void handleComputeClusters(List<Cloud.ClusterPB> computeClusters) {
@@ -179,15 +199,23 @@ public class CloudInstanceStatusChecker extends
MasterDaemon {
}
}
- private void handleVirtualClusters(List<Cloud.ClusterPB> virtualGroups,
List<Cloud.ClusterPB> computeClusters) {
+ private List<Cloud.ClusterPB> handleVirtualClusters(
+ List<Cloud.ClusterPB> virtualGroups, List<Cloud.ClusterPB>
computeClusters) {
+ List<Cloud.ClusterPB> reconciledGroups = new ArrayList<>();
for (Cloud.ClusterPB virtualGroupInMs : virtualGroups) {
CloudComputeGroupMeta virtualGroupInFe = cloudSystemInfoService
.getComputeGroupById(virtualGroupInMs.getClusterId());
+ boolean reconciled;
if (virtualGroupInFe != null) {
- handleExistingVirtualComputeGroup(virtualGroupInMs,
virtualGroupInFe);
+ reconciled =
handleExistingVirtualComputeGroup(virtualGroupInMs, virtualGroupInFe);
} else {
- handleNewVirtualComputeGroup(virtualGroupInMs,
computeClusters);
+ reconciled = handleNewVirtualComputeGroup(virtualGroupInMs,
computeClusters);
+ }
+ // Rejected renames must not publish a name for metadata whose
update was rejected.
+ if (!reconciled) {
+ continue;
}
+ reconciledGroups.add(virtualGroupInMs);
// just fe master gen file cache sync task
if (Env.getCurrentEnv().isMaster()) {
// get again in fe mem
@@ -201,6 +229,7 @@ public class CloudInstanceStatusChecker extends
MasterDaemon {
syncFileCacheTasksForVirtualGroup(virtualGroupInMs,
virtualGroupInFe);
}
}
+ return reconciledGroups;
}
private void cancelCacheJobs(CloudComputeGroupMeta vcgInFe, List<String>
jobIds) {
@@ -349,21 +378,22 @@ public class CloudInstanceStatusChecker extends
MasterDaemon {
}
}
- private void handleExistingVirtualComputeGroup(
+ private boolean handleExistingVirtualComputeGroup(
Cloud.ClusterPB clusterInMs, CloudComputeGroupMeta
virtualGroupInFe) {
if (!isClusterIdConsistent(clusterInMs, virtualGroupInFe)) {
- return;
+ return false;
}
if (!isClusterPolicyValid(clusterInMs)) {
- return;
+ return false;
}
if (!areSubComputeGroupsValid(clusterInMs, virtualGroupInFe)) {
- return;
+ return false;
}
diffAndUpdateComputeGroup(clusterInMs, virtualGroupInFe);
+ return true;
}
private boolean isClusterIdConsistent(Cloud.ClusterPB cluster,
CloudComputeGroupMeta computeGroup) {
@@ -478,27 +508,27 @@ public class CloudInstanceStatusChecker extends
MasterDaemon {
}
}
- private void handleNewVirtualComputeGroup(Cloud.ClusterPB cluster,
List<Cloud.ClusterPB> computeClusters) {
+ private boolean handleNewVirtualComputeGroup(Cloud.ClusterPB cluster,
List<Cloud.ClusterPB> computeClusters) {
List<String> subComputeGroups = cluster.getClusterNamesList();
if (subComputeGroups.isEmpty()) {
LOG.info("found virtual cluster {} which has no sub clusters, skip
empty virtual cluster", cluster);
- return;
+ return false;
}
if (subComputeGroups.size() != 2) {
LOG.warn("virtual compute err, sub compute group size not eq 2, in
ms {}", subComputeGroups);
- return;
+ return false;
}
if (!cluster.hasClusterPolicy()) {
LOG.warn("virtual compute err, no cluster policy {}", cluster);
- return;
+ return false;
}
if (!cluster.getClusterPolicy().hasActiveClusterName()) {
LOG.warn("virtual compute err, active cluster empty in ms {}",
cluster);
- return;
+ return false;
}
if (cluster.getClusterPolicy().getStandbyClusterNamesList().size() !=
1) {
LOG.warn("virtual compute err, standby cluster size not eq 1 in ms
{}", cluster);
- return;
+ return false;
}
checkSubClusters(subComputeGroups, cluster, computeClusters);
CloudComputeGroupMeta computeGroup = new
CloudComputeGroupMeta(cluster.getClusterId(),
@@ -513,6 +543,7 @@ public class CloudInstanceStatusChecker extends
MasterDaemon {
computeGroup.setNeedRebuildFileCache(true);
cloudSystemInfoService.addComputeGroup(cluster.getClusterId(),
computeGroup);
MetricRepo.registerCloudMetrics(cluster.getClusterId(),
cluster.getClusterName());
+ return true;
}
private void checkSubClusters(List<String> subClusterNames,
Cloud.ClusterPB cluster,
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 4c51c65457e..4c4bcb175a8 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
@@ -643,7 +643,7 @@ public class CloudReplica extends Replica implements
GsonPostProcessable {
*
* Such an entry is already dead weight: getBackendIdImpl() resolves the
backend id, gets null and
* falls back to hashReplicaToBe(), so removing it does not change
routing. But nothing ever removes
- * it either -- dropCluster() only touches CloudSystemInfoService, and the
rebalancer only walks the
+ * it either -- removeComputeGroup() only touches CloudSystemInfoService,
and the rebalancer only walks the
* compute groups that currently exist -- so entries of dropped compute
groups pile up forever, both
* in FE heap and in the image (the `bes`/`be` field).
*
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/cloud/system/CloudSystemInfoService.java
b/fe/fe-core/src/main/java/org/apache/doris/cloud/system/CloudSystemInfoService.java
index 104489572aa..05c4e04e7de 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/cloud/system/CloudSystemInfoService.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/cloud/system/CloudSystemInfoService.java
@@ -68,6 +68,7 @@ import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
@@ -249,17 +250,46 @@ public class CloudSystemInfoService extends
SystemInfoService {
clusterIdToBackend.computeIfAbsent(clusterId, k -> new ArrayList<>());
}
- public void removeVirtualClusterInfoFromMapsNoLock(String clusterId,
String clusterName) {
- LOG.info("remove virtual cluster info from maps, clusterId={},
clusterName={}", clusterId, clusterName);
- clusterIdToBackend.remove(clusterId);
- clusterNameToId.remove(clusterName);
- }
-
public void renameVirtualClusterInfoFromMapsNoLock(String clusterId,
String oldClusterName, String newClusterName) {
LOG.info("remove virtual cluster info from maps, clusterId={}, name
from {} to {}",
clusterId, oldClusterName, newClusterName);
clusterNameToId.put(newClusterName, clusterId);
- clusterNameToId.remove(oldClusterName);
+ clusterNameToId.remove(oldClusterName, clusterId);
+ }
+
+ // Physical and virtual group checkers fetch and apply snapshots
independently.
+ // For example:
+ // 1. A current physical snapshot installs name -> newId.
+ // 2. A delayed virtual snapshot overwrites it with name -> oldId.
+ // 3. A later virtual snapshot removes oldId and its name mapping.
+ // The new physical group's metadata and BEs still exist, but its name
mapping
+ // is missing. Normal add/rename detection cannot repair it because neither
+ // the group ID nor the BE names have changed.
+ //
+ // Reconcile name mappings for locally installed groups on every sync
cycle.
+ // Callers must exclude records whose metadata reconciliation was rejected.
+ // This does not reject stale snapshots; it restores the current mapping
once
+ // a later cycle applies current metadata from the meta service.
+ public void refreshComputeGroupNames(Collection<Cloud.ClusterPB>
remoteComputeGroups) {
+ wlock.lock();
+ try {
+ for (Cloud.ClusterPB group : remoteComputeGroups) {
+ String id = group.getClusterId();
+ // Empty physical groups and rejected virtual groups may not
be installed locally.
+ // Do not create a name pointing to missing metadata.
+ if (!computeGroupIdToComputeGroup.containsKey(id)) {
+ continue;
+ }
+ String name = group.getClusterName();
+ String previousId = clusterNameToId.put(name, id);
+ if (!id.equals(previousId)) {
+ LOG.warn("repair compute group name mapping from meta
service, name={}, oldId={}, currentId={}",
+ name, previousId, id);
+ }
+ }
+ } finally {
+ wlock.unlock();
+ }
}
public CloudComputeGroupMeta getComputeGroupByName(String
computeGroupName) {
@@ -423,11 +453,17 @@ public class CloudSystemInfoService extends
SystemInfoService {
}
}
+ // Remove local metadata for a physical or virtual compute group. Physical
backends
+ // must be removed by the caller before removing the group.
public void removeComputeGroup(String computeGroupId, String
computeGroupName) {
try {
wlock.lock();
+ LOG.info("remove compute group, id={}, name={}", computeGroupId,
computeGroupName);
computeGroupIdToComputeGroup.remove(computeGroupId);
- removeVirtualClusterInfoFromMapsNoLock(computeGroupId,
computeGroupName);
+ clusterIdToBackend.remove(computeGroupId);
+ // Earlier checkers could publish aliases from rejected renames.
Remove every name
+ // still owned by this ID, without removing names already reused
by another group.
+ clusterNameToId.entrySet().removeIf(entry ->
computeGroupId.equals(entry.getValue()));
invalidateCloudColocatePlacement(computeGroupId);
} finally {
wlock.unlock();
@@ -634,22 +670,13 @@ public class CloudSystemInfoService extends
SystemInfoService {
be = be.stream().filter(i ->
!d.contains(i.getId())).collect(Collectors.toList());
// ATTN: clusterId may have zero nodes
clusterIdToBackend.replace(clusterId, be);
- // such as dropCluster, but no lock
// ATTN: Empty clusters are treated as dropped clusters.
if (be.isEmpty()) {
LOG.info("del clusterId {} and clusterName {} due to be nodes
eq 0", clusterId, clusterName);
MetricRepo.unregisterCloudMetrics(clusterId, clusterName,
toDel);
- boolean succ = clusterNameToId.remove(clusterName, clusterId);
// remove from computeGroupIdToComputeGroup
removeComputeGroup(clusterId, clusterName);
-
- if (!succ) {
- LOG.warn("impossible, somewhere err, clusterNameToId {}, "
- + "want remove cluster name {}, cluster id {}",
- clusterNameToId, clusterName, clusterId);
- }
- clusterIdToBackend.remove(clusterId);
}
LOG.info("update (del) cloud cluster map, clusterName={}
clusterId={} backendNum={} current backend={}",
clusterName, clusterId, be.size(), b);
@@ -1232,7 +1259,7 @@ public class CloudSystemInfoService extends
SystemInfoService {
final String originalName, final String
clusterId) {
wlock.lock();
try {
- clusterNameToId.remove(originalName);
+ clusterNameToId.remove(originalName, clusterId);
clusterNameToId.put(newName, clusterId);
} finally {
wlock.unlock();
@@ -1264,17 +1291,6 @@ public class CloudSystemInfoService extends
SystemInfoService {
return clusterName;
}
- public void dropCluster(final String clusterId, final String clusterName) {
- wlock.lock();
- try {
- clusterNameToId.remove(clusterName, clusterId);
- clusterIdToBackend.remove(clusterId);
- invalidateCloudColocatePlacement(clusterId);
- } finally {
- wlock.unlock();
- }
- }
-
public List<String> getCloudClusterNames() {
rlock.lock();
try {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudClusterCheckerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudClusterCheckerTest.java
new file mode 100644
index 00000000000..2e71cfcfb98
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudClusterCheckerTest.java
@@ -0,0 +1,308 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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
+//
+// http://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 org.apache.doris.cloud.catalog;
+
+import org.apache.doris.catalog.Env;
+import org.apache.doris.cloud.CacheHotspotManager;
+import org.apache.doris.cloud.proto.Cloud;
+import org.apache.doris.cloud.system.CloudSystemInfoService;
+import org.apache.doris.common.Config;
+import org.apache.doris.persist.EditLog;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+class CloudClusterCheckerTest {
+ private String originalCloudUniqueId;
+ private CloudSystemInfoService service;
+ private CloudEnv env;
+ private MockedStatic<Env> mockedEnv;
+
+ @BeforeEach
+ void setUp() {
+ originalCloudUniqueId = Config.cloud_unique_id;
+ Config.cloud_unique_id = "test_cloud";
+ service = Mockito.spy(new CloudSystemInfoService());
+ env = Mockito.mock(CloudEnv.class);
+ AtomicLong nextId = new AtomicLong(10000);
+ Mockito.when(env.getNextId()).thenAnswer(invocation ->
nextId.incrementAndGet());
+ Mockito.when(env.getEditLog()).thenReturn(Mockito.mock(EditLog.class));
+
Mockito.when(env.getCacheHotspotMgr()).thenReturn(Mockito.mock(CacheHotspotManager.class));
+ mockedEnv = mockEnv();
+ }
+
+ private MockedStatic<Env> mockEnv() {
+ MockedStatic<Env> mock = Mockito.mockStatic(Env.class);
+ mock.when(Env::getCurrentEnv).thenReturn(env);
+ mock.when(Env::getCurrentSystemInfo).thenReturn(service);
+ return mock;
+ }
+
+ @AfterEach
+ void tearDown() {
+ mockedEnv.close();
+ Config.cloud_unique_id = originalCloudUniqueId;
+ }
+
+ @Test
+ void testCheckerDeletesRecreatedGroupById() throws Exception {
+ Cloud.ClusterPB oldGroup = physical("old", "reused", "127.0.0.1");
+ Cloud.ClusterPB newGroup = physical("new", "reused", "127.0.0.2");
+ syncPhysical(oldGroup);
+ long oldBackendId =
service.getCloudClusterIdToBackend(false).get("old").get(0).getId();
+
+ syncPhysical(newGroup);
+
+ Assertions.assertNull(service.getBackend(oldBackendId));
+ Assertions.assertNull(service.getComputeGroupById("old"));
+
Assertions.assertFalse(service.getCloudClusterIdToBackend(false).containsKey("old"));
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ syncPhysical(newGroup);
+ Assertions.assertEquals(1,
service.getAllBackendsByAllCluster().size());
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ }
+
+ @Test
+ void testCheckerRenamesGroupAfterOldNameIsReused() throws Exception {
+ Cloud.ClusterPB oldGroup = physical("old", "reused", "127.0.0.1");
+ Cloud.ClusterPB renamed =
oldGroup.toBuilder().setClusterName("renamed").build();
+ Cloud.ClusterPB replacement = physical("new", "reused", "127.0.0.2");
+ syncPhysical(oldGroup);
+ syncPhysical(renamed, replacement);
+
+ Assertions.assertEquals("old",
service.getCloudClusterIdByName("renamed"));
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ syncPhysical(renamed, replacement);
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ }
+
+ @Test
+ void testPhysicalRenameDoesNotRemoveAnotherGroupsName() {
+ service.addComputeGroup("old", new CloudComputeGroupMeta("old",
"reused",
+ CloudComputeGroupMeta.ComputeTypeEnum.COMPUTE));
+ service.addComputeGroup("new", new CloudComputeGroupMeta("new",
"reused",
+ CloudComputeGroupMeta.ComputeTypeEnum.COMPUTE));
+ service.updateClusterNameToId("renamed", "reused", "old");
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ Assertions.assertEquals("old",
service.getCloudClusterIdByName("renamed"));
+ }
+
+ @Test
+ void testVirtualRenameDoesNotRemoveAnotherGroupsName() {
+ service.addComputeGroup("old", new CloudComputeGroupMeta("old",
"reused",
+ CloudComputeGroupMeta.ComputeTypeEnum.VIRTUAL));
+ service.addComputeGroup("new", new CloudComputeGroupMeta("new",
"reused",
+ CloudComputeGroupMeta.ComputeTypeEnum.VIRTUAL));
+ service.renameVirtualComputeGroup("old", "reused", new
CloudComputeGroupMeta("old", "renamed",
+ CloudComputeGroupMeta.ComputeTypeEnum.VIRTUAL));
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ Assertions.assertEquals("old",
service.getCloudClusterIdByName("renamed"));
+ }
+
+ @Test
+ void testLaterCycleRepairsNameAfterStaleVirtualSnapshot() throws Exception
{
+ Cloud.ClusterPB active = physical("active", "active", "127.0.0.1");
+ Cloud.ClusterPB standby = physical("standby", "standby", "127.0.0.2");
+ Cloud.ClusterPB replacement = physical("new", "reused", "127.0.0.3");
+ syncPhysical(active, standby);
+ Cloud.GetInstanceResponse stale = instance(active, standby,
virtual("old", "reused"));
+ CountDownLatch responseFetched = new CountDownLatch(1);
+ CountDownLatch applyResponse = new CountDownLatch(1);
+ Mockito.doAnswer(invocation -> {
+ responseFetched.countDown();
+ Assertions.assertTrue(applyResponse.await(30, TimeUnit.SECONDS));
+ return stale;
+ }).when(service).getCloudInstance();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ Future<?> staleCycle = executor.submit(() -> {
+ try (MockedStatic<Env> ignored = mockEnv()) {
+ new
CloudInstanceStatusChecker(service).runAfterCatalogReady();
+ }
+ });
+ Assertions.assertTrue(responseFetched.await(30, TimeUnit.SECONDS));
+ syncPhysical(active, standby, replacement);
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ applyResponse.countDown();
+ staleCycle.get(30, TimeUnit.SECONDS);
+ Assertions.assertEquals("old",
service.getCloudClusterIdByName("reused"));
+
+ // A current physical snapshot must repair even a non-empty, wrong
mapping.
+ syncPhysical(active, standby, replacement);
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ syncInstance(active, standby, replacement);
+ Assertions.assertNull(service.getComputeGroupById("old"));
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ syncPhysical(active, standby, replacement);
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+ } finally {
+ applyResponse.countDown();
+ executor.shutdownNow();
+ Assertions.assertTrue(executor.awaitTermination(30,
TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ void testLaterCyclesRepairMissingPhysicalAndVirtualNames() throws
Exception {
+ Cloud.ClusterPB active = physical("active", "active", "127.0.0.1");
+ Cloud.ClusterPB standby = physical("standby", "standby", "127.0.0.2");
+ Cloud.ClusterPB current = physical("new", "reused", "127.0.0.3");
+ syncPhysical(active, standby, current);
+ syncInstance(active, standby, virtual("old", "reused"));
+ syncInstance(active, standby, current);
+ Assertions.assertNull(service.getCloudClusterIdByName("reused"));
+ syncPhysical(active, standby, current);
+ Assertions.assertEquals("new",
service.getCloudClusterIdByName("reused"));
+
+ // Reverse the types: an obsolete physical snapshot overwrites a
current virtual group.
+ syncPhysical(active, standby);
+ Cloud.ClusterPB currentVirtual = virtual("virtual", "reused");
+ syncInstance(active, standby, currentVirtual);
+ syncPhysical(active, standby, current);
+ syncPhysical(active, standby);
+ Assertions.assertNull(service.getCloudClusterIdByName("reused"));
+ syncInstance(active, standby, currentVirtual);
+ Assertions.assertEquals("virtual",
service.getCloudClusterIdByName("reused"));
+ syncInstance(active, standby, currentVirtual);
+ Assertions.assertEquals("virtual",
service.getCloudClusterIdByName("reused"));
+ Assertions.assertNull(service.getComputeGroupById("new"));
+ }
+
+ @Test
+ void testRejectedVirtualPolicyDoesNotPublishRenamedGroup() throws
Exception {
+ assertRejectedVirtualRename(virtual("virtual", "renamed").toBuilder()
+ .clearClusterPolicy().build());
+ }
+
+ @Test
+ void testRejectedVirtualSubgroupsDoNotPublishRenamedGroup() throws
Exception {
+ assertRejectedVirtualRename(virtual("virtual", "renamed").toBuilder()
+ .clearClusterNames().addClusterNames("active").build());
+ }
+
+ private void assertRejectedVirtualRename(Cloud.ClusterPB rejected) throws
Exception {
+ Cloud.ClusterPB active = physical("active", "active", "127.0.0.1");
+ Cloud.ClusterPB standby = physical("standby", "standby", "127.0.0.2");
+ syncPhysical(active, standby);
+ syncInstance(active, standby, virtual("virtual", "original"));
+
+ syncInstance(active, standby, rejected);
+
+ Assertions.assertEquals("original",
service.getComputeGroupById("virtual").getName());
+ Assertions.assertEquals("virtual",
service.getCloudClusterIdByName("original"));
+ Assertions.assertNull(service.getCloudClusterIdByName("renamed"));
+
+ // The rejected record must not make the still-present group look
obsolete.
+ // Once it actually disappears from MS, neither name may point to its
removed ID.
+ syncInstance(active, standby);
+ Assertions.assertNull(service.getComputeGroupById("virtual"));
+ Assertions.assertNull(service.getCloudClusterIdByName("original"));
+ Assertions.assertNull(service.getCloudClusterIdByName("renamed"));
+ }
+
+ @Test
+ void testAcceptedVirtualRenamePublishesNewName() throws Exception {
+ Cloud.ClusterPB active = physical("active", "active", "127.0.0.1");
+ Cloud.ClusterPB standby = physical("standby", "standby", "127.0.0.2");
+ syncPhysical(active, standby);
+ syncInstance(active, standby, virtual("virtual", "original"));
+
+ syncInstance(active, standby, virtual("virtual", "renamed"));
+
+ Assertions.assertEquals("renamed",
service.getComputeGroupById("virtual").getName());
+ Assertions.assertEquals("virtual",
service.getCloudClusterIdByName("renamed"));
+ Assertions.assertNull(service.getCloudClusterIdByName("original"));
+ }
+
+ @Test
+ void testRejectedNewVirtualGroupDoesNotPublishName() {
+ syncInstance(virtual("virtual",
"rejected").toBuilder().clearClusterPolicy().build());
+
+ Assertions.assertNull(service.getComputeGroupById("virtual"));
+ Assertions.assertNull(service.getCloudClusterIdByName("rejected"));
+ }
+
+ @Test
+ void testVirtualRemovalClearsStaleAliasesButKeepsReusedName() throws
Exception {
+ Cloud.ClusterPB active = physical("active", "active", "127.0.0.1");
+ Cloud.ClusterPB standby = physical("standby", "standby", "127.0.0.2");
+ syncPhysical(active, standby);
+ syncInstance(active, standby, virtual("virtual", "reused"));
+ // Simulate an alias left by an earlier checker that refreshed a
rejected rename.
+ service.addVirtualClusterInfoToMapsNoLock("virtual", "stale_alias");
+ syncPhysical(active, standby, physical("replacement", "reused",
"127.0.0.3"));
+
+ syncInstance(active, standby);
+
+ Assertions.assertNull(service.getComputeGroupById("virtual"));
+ Assertions.assertNull(service.getCloudClusterIdByName("stale_alias"));
+ Assertions.assertEquals("replacement",
service.getCloudClusterIdByName("reused"));
+ }
+
+ private void syncPhysical(Cloud.ClusterPB... groups) throws Exception {
+ Mockito.doReturn(Cloud.GetClusterResponse.newBuilder().setStatus(ok())
+
.addAllCluster(Arrays.asList(groups)).build()).when(service).getCloudCluster("",
"", "");
+ // The production reconciliation entry, including add, delete, diff
and validation.
+ Method method =
CloudClusterChecker.class.getDeclaredMethod("checkCloudBackends");
+ method.setAccessible(true);
+ method.invoke(new CloudClusterChecker(service));
+ }
+
+ private void syncInstance(Cloud.ClusterPB... groups) {
+ Mockito.doReturn(instance(groups)).when(service).getCloudInstance();
+ new CloudInstanceStatusChecker(service).runAfterCatalogReady();
+ }
+
+ private Cloud.GetInstanceResponse instance(Cloud.ClusterPB... groups) {
+ return Cloud.GetInstanceResponse.newBuilder().setStatus(ok())
+
.setInstance(Cloud.InstanceInfoPB.newBuilder().setStatus(Cloud.InstanceInfoPB.Status.NORMAL)
+ .addAllClusters(Arrays.asList(groups))).build();
+ }
+
+ private Cloud.MetaServiceResponseStatus ok() {
+ return
Cloud.MetaServiceResponseStatus.newBuilder().setCode(Cloud.MetaServiceCode.OK).build();
+ }
+
+ private Cloud.ClusterPB physical(String id, String name, String ip) {
+ return
Cloud.ClusterPB.newBuilder().setClusterId(id).setClusterName(name)
+
.setType(Cloud.ClusterPB.Type.COMPUTE).setClusterStatus(Cloud.ClusterStatus.NORMAL)
+
.addNodes(Cloud.NodeInfoPB.newBuilder().setIp(ip).setHeartbeatPort(9050)
+
.setCloudUniqueId(id).setStatus(Cloud.NodeStatusPB.NODE_STATUS_RUNNING)).build();
+ }
+
+ private Cloud.ClusterPB virtual(String id, String name) {
+ return
Cloud.ClusterPB.newBuilder().setClusterId(id).setClusterName(name)
+
.setType(Cloud.ClusterPB.Type.VIRTUAL).addAllClusterNames(Arrays.asList("active",
"standby"))
+
.setClusterPolicy(Cloud.ClusterPolicy.newBuilder().setType(Cloud.ClusterPolicy.PolicyType.ActiveStandby)
+ .setActiveClusterName("active")
+ .addStandbyClusterNames("standby")).build();
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/system/CloudSystemInfoServiceTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/system/CloudSystemInfoServiceTest.java
index 194bc6a8a59..c74138023e0 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/cloud/system/CloudSystemInfoServiceTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/system/CloudSystemInfoServiceTest.java
@@ -36,6 +36,7 @@ import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -1062,6 +1063,49 @@ public class CloudSystemInfoServiceTest {
Assert.assertFalse(infoService.containsCloudCluster("cluster_2"));
}
+ @Test
+ public void testRemovingRecreatedComputeGroupKeepsCurrentNameMapping() {
+ infoService = new CloudSystemInfoService();
+ String clusterName = "recreated_cluster";
+ String oldClusterId = "old_cluster_id";
+ String newClusterId = "new_cluster_id";
+
+ Backend oldBackend = new Backend(1L, "127.0.0.1", 9050);
+ Map<String, String> oldTagMap = Tag.DEFAULT_BACKEND_TAG.toMap();
+ oldTagMap.put(Tag.CLOUD_CLUSTER_NAME, clusterName);
+ oldTagMap.put(Tag.CLOUD_CLUSTER_ID, oldClusterId);
+ oldBackend.setTagMap(oldTagMap);
+ infoService.updateCloudClusterMapNoLock(Arrays.asList(oldBackend), new
ArrayList<>());
+
+ Backend newBackend = new Backend(2L, "127.0.0.2", 9050);
+ Map<String, String> newTagMap = Tag.DEFAULT_BACKEND_TAG.toMap();
+ newTagMap.put(Tag.CLOUD_CLUSTER_NAME, clusterName);
+ newTagMap.put(Tag.CLOUD_CLUSTER_ID, newClusterId);
+ newBackend.setTagMap(newTagMap);
+ infoService.updateCloudClusterMapNoLock(Arrays.asList(newBackend), new
ArrayList<>());
+
+ infoService.updateCloudClusterMapNoLock(new ArrayList<>(),
Arrays.asList(oldBackend));
+
+ Assert.assertEquals(newClusterId,
infoService.getCloudClusterIdByName(clusterName));
+ Assert.assertNull(infoService.getComputeGroupById(oldClusterId));
+ Assert.assertNotNull(infoService.getComputeGroupById(newClusterId));
+ }
+
+ @Test
+ public void testRemovingCurrentComputeGroupRemovesNameMapping() {
+ infoService = new CloudSystemInfoService();
+ String clusterName = "removed_cluster";
+ String clusterId = "removed_cluster_id";
+ CloudComputeGroupMeta computeGroup = new CloudComputeGroupMeta(
+ clusterId, clusterName,
CloudComputeGroupMeta.ComputeTypeEnum.COMPUTE);
+ infoService.addComputeGroup(clusterId, computeGroup);
+
+ infoService.removeComputeGroup(clusterId, clusterName);
+
+ Assert.assertNull(infoService.getCloudClusterIdByName(clusterName));
+ Assert.assertNull(infoService.getComputeGroupById(clusterId));
+ }
+
/**
* Helper method to create a test ConnectContext with specific cluster name
*/
diff --git
a/regression-test/data/cloud_p0/node_mgr/test_compute_group_name_reconciliation.out
b/regression-test/data/cloud_p0/node_mgr/test_compute_group_name_reconciliation.out
new file mode 100644
index 00000000000..889a512b525
--- /dev/null
+++
b/regression-test/data/cloud_p0/node_mgr/test_compute_group_name_reconciliation.out
@@ -0,0 +1,21 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !recreated --
+1
+2
+3
+
+-- !renamed --
+1
+2
+3
+
+-- !reused --
+1
+2
+3
+
+-- !repaired --
+1
+2
+3
+
diff --git
a/regression-test/suites/cloud_p0/node_mgr/test_compute_group_name_reconciliation.groovy
b/regression-test/suites/cloud_p0/node_mgr/test_compute_group_name_reconciliation.groovy
new file mode 100644
index 00000000000..ac1e7b586f3
--- /dev/null
+++
b/regression-test/suites/cloud_p0/node_mgr/test_compute_group_name_reconciliation.groovy
@@ -0,0 +1,169 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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
+//
+// http://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.
+
+import groovy.json.JsonOutput
+import org.apache.doris.regression.suite.ClusterOptions
+
+// Run with a cloud image containing the checker debug points below. For a
pre-fix
+// reproduction, keep the debug points but revert the reconciliation changes.
+suite('test_compute_group_name_reconciliation', 'cloud_p0,docker') {
+ if (!isCloudMode()) {
+ return
+ }
+
+ def options = new ClusterOptions()
+ options.cloudMode = true
+ options.setFeNum(1)
+ options.setBeNum(1)
+ options.enableDebugPoints()
+ options.feConfigs += ['cloud_cluster_check_interval_second=1',
'heartbeat_interval_second=1']
+
+ docker(options) {
+ def ms = cluster.getAllMetaservices().get(0)
+ def msEndpoint = "${ms.host}:${ms.httpPort}"
+ def feLog = new File(cluster.getFeByIndex(1).getLogFilePath())
+ def physicalPause = 'CloudClusterChecker.checkCloudBackends.pause'
+ def instancePause =
'CloudInstanceStatusChecker.runAfterCatalogReady.pause'
+ def stalePause = 'CloudInstanceStatusChecker.afterGetInstance.pause'
+ def debug = GetDebugPoint()
+
+ def alterCluster = { operation, group ->
+ httpTest {
+ endpoint msEndpoint
+ uri "/MetaService/http/${operation}?token=${token}"
+ body JsonOutput.toJson([instance_id: 'default_instance_id',
cluster: group])
+ check { code, response ->
+ def result = parseJson(response)
+ assertTrue(code == 200 && result.code == 'OK',
"${operation}: ${response}")
+ }
+ }
+ }
+ def remoteGroup = { name ->
+ def group = get_instance(ms).clusters.find { it.cluster_name ==
name }
+ assertNotNull(group, "remote compute group ${name}")
+ group
+ }
+ def pause = { point, phase, params = [:] ->
+ debug.enableDebugPointForAllFEs(point, params + [phase: phase,
timeout: '180'])
+ // A unique phase proves this cycle reached the pause; no timing
guesses.
+ awaitUntil(30) { feLog.text.contains("${point} phase=${phase}") }
+ }
+ def resume = { point -> debug.disableDebugPointForAllFEs(point) }
+ def waitPhysical = { name, id, removedIds ->
+ awaitUntil(60) {
+ def bes = sql_return_maparray('SHOW BACKENDS')
+ def tags = bes.collect { parseJson(it.Tag) }
+ def groups = sql_return_maparray('SHOW COMPUTE GROUPS')
+ tags.any { it.compute_group_id == id && it.compute_group_name
== name } &&
+ !tags.any { removedIds.contains(it.compute_group_id) }
&&
+ groups.any { it.Name == name &&
it.BackendNum.toInteger() == 1 }
+ }
+ sql "USE @${name}"
+ }
+
+ try {
+ cluster.addBackend(1, 'cg_reused')
+ cluster.addBackend(1, 'cg_spare')
+ def old = remoteGroup('cg_reused')
+ def spare = remoteGroup('cg_spare')
+ waitPhysical('cg_reused', old.cluster_id, [])
+ waitPhysical('cg_spare', spare.cluster_id, [])
+ sql 'USE @compute_cluster'
+ sql 'DROP TABLE IF EXISTS test_compute_group_name_reconciliation'
+ sql '''CREATE TABLE test_compute_group_name_reconciliation (k INT)
+ DISTRIBUTED BY HASH(k) BUCKETS 1 PROPERTIES
('replication_num'='1')'''
+ sql 'INSERT INTO test_compute_group_name_reconciliation VALUES
(1), (2), (3)'
+
+ // 1. A single checker cycle sees a same-name replacement on
distinct BEs.
+ pause(physicalPause, 'recreate')
+ alterCluster('drop_cluster', [cluster_id: old.cluster_id,
cluster_name: 'cg_reused'])
+ alterCluster('drop_cluster', [cluster_id: spare.cluster_id,
cluster_name: 'cg_spare'])
+ alterCluster('add_cluster', [cluster_id: 'cg_recreated_id',
cluster_name: 'cg_reused',
+ type: 'COMPUTE', nodes: spare.nodes])
+ resume(physicalPause)
+ waitPhysical('cg_reused', 'cg_recreated_id', [old.cluster_id,
spare.cluster_id])
+ order_qt_recreated 'SELECT k FROM
test_compute_group_name_reconciliation'
+
+ // 2. Rename A and reuse its old name for B before the next
checker cycle.
+ pause(physicalPause, 'rename')
+ alterCluster('rename_cluster', [cluster_id: 'cg_recreated_id',
cluster_name: 'cg_renamed'])
+ alterCluster('add_cluster', [cluster_id:
'cg_reuse_after_rename_id', cluster_name: 'cg_reused',
+ type: 'COMPUTE', nodes: old.nodes])
+ resume(physicalPause)
+ waitPhysical('cg_renamed', 'cg_recreated_id', [old.cluster_id,
spare.cluster_id])
+ order_qt_renamed 'SELECT k FROM
test_compute_group_name_reconciliation'
+ waitPhysical('cg_reused', 'cg_reuse_after_rename_id',
[old.cluster_id, spare.cluster_id])
+ order_qt_reused 'SELECT k FROM
test_compute_group_name_reconciliation'
+
+ // 3. Hold an old virtual-group response while the physical
checker installs
+ // a newer same-name physical group. Then let the old response
overwrite it.
+ cluster.addBackend(1, 'cg_snapshot_spare')
+ def snapshotSpare = remoteGroup('cg_snapshot_spare')
+ waitPhysical('cg_snapshot_spare', snapshotSpare.cluster_id, [])
+ sql 'USE @compute_cluster'
+ pause(instancePause, 'before_virtual')
+ alterCluster('add_cluster', [cluster_id: 'cg_stale_virtual_id',
cluster_name: 'cg_snapshot',
+ type: 'VIRTUAL', cluster_names: ['compute_cluster',
'cg_renamed'],
+ cluster_policy: [type: 'ActiveStandby',
active_cluster_name: 'compute_cluster',
+ standby_cluster_names: ['cg_renamed']]])
+ debug.enableDebugPointForAllFEs(stalePause,
+ [cluster_id: 'cg_stale_virtual_id', phase:
'stale_response', timeout: '180'])
+ resume(instancePause)
+ awaitUntil(30) { feLog.text.contains("${stalePause}
phase=stale_response") }
+ alterCluster('drop_cluster', [cluster_id: 'cg_stale_virtual_id',
cluster_name: 'cg_snapshot'])
+ alterCluster('drop_cluster', [cluster_id:
snapshotSpare.cluster_id, cluster_name: 'cg_snapshot_spare'])
+ alterCluster('add_cluster', [cluster_id: 'cg_current_physical_id',
cluster_name: 'cg_snapshot',
+ type: 'COMPUTE', nodes: snapshotSpare.nodes])
+ waitPhysical('cg_snapshot', 'cg_current_physical_id',
[snapshotSpare.cluster_id])
+ sql 'USE @compute_cluster'
+ pause(physicalPause, 'hold_repair')
+ debug.enableDebugPointForAllFEs(instancePause, [phase:
'after_stale', timeout: '180'])
+ resume(stalePause)
+ awaitUntil(30) { feLog.text.contains("${instancePause}
phase=after_stale") }
+ awaitUntil(30) {
+ sql_return_maparray('SHOW COMPUTE GROUPS').any {
+ it.Name == 'cg_snapshot' &&
it.SubComputeGroups.contains('cg_renamed')
+ }
+ }
+
+ // Current instance data removes V. P's BEs remain, but its name
is now missing.
+ resume(instancePause)
+ awaitUntil(60) {
+ !sql_return_maparray('SHOW COMPUTE GROUPS').any { it.Name ==
'cg_snapshot' }
+ }
+ // No node or name change in MS: only periodic mapping repair can
restore P.
+ resume(physicalPause)
+ waitPhysical('cg_snapshot', 'cg_current_physical_id',
[snapshotSpare.cluster_id])
+ order_qt_repaired 'SELECT k FROM
test_compute_group_name_reconciliation'
+
+ // Check stability after several subsequent physical AND instance
cycles.
+ 3.times {
+ def logOffset = feLog.text.length()
+ awaitUntil(30) {
+ def subsequent = feLog.text.substring(logOffset)
+ subsequent.contains('daemon cluster get cluster info
succ') &&
+ subsequent.contains('finished to cloud instance
checker')
+ }
+ waitPhysical('cg_snapshot', 'cg_current_physical_id',
[snapshotSpare.cluster_id])
+ }
+ } finally {
+ resume(stalePause)
+ resume(instancePause)
+ resume(physicalPause)
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]