This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 8e2a55e3f31 branch-4.1: [test](regression) Add warmup mixed-mode 
coverage and debug observability #65153 (#65568)
8e2a55e3f31 is described below

commit 8e2a55e3f3139c23635835134986ad9ce68219f1
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Jul 15 12:08:56 2026 +0800

    branch-4.1: [test](regression) Add warmup mixed-mode coverage and debug 
observability #65153 (#65568)
    
    Cherry-picked from #65153
    
    Co-authored-by: chunping <[email protected]>
---
 docker/runtime/doris-compose/utils.py              |  36 ++-
 .../apache/doris/cloud/CacheHotspotManager.java    |  40 ++-
 .../org/apache/doris/cloud/CloudWarmUpJob.java     |  15 +-
 .../cloud/catalog/CloudInstanceStatusChecker.java  |  19 +-
 .../cloud/CacheHotspotManagerTableFilterTest.java  |  61 ++++
 .../doris/cloud/cache/CacheHotspotManagerTest.java |  46 +++
 .../catalog/CloudInstanceStatusCheckerTest.java    |   5 +-
 .../regression/util/WarmupMetricsUtils.groovy      | 310 ++++++++++++++++++++-
 ...p_cluster_event_conflict_and_diagnostics.groovy | 111 ++++++++
 ...test_warm_up_cluster_periodic_add_new_be.groovy | 184 +++++++++---
 ..._up_cluster_periodic_scheduler_semantics.groovy | 137 +++++++++
 ...arm_up_mixed_cluster_change_and_failover.groovy | 117 ++++++++
 .../test_warm_up_normal_queue_semantics.groovy     | 146 ++++++++++
 ...arm_up_event_on_tables_overlap_semantics.groovy | 125 +++++++++
 ...t_vcg_warmup_allows_manual_periodic_jobs.groovy | 103 +++++++
 ...est_vcg_warmup_failover_cancels_old_jobs.groovy | 125 +++++++++
 ...test_vcg_warmup_precedence_and_reconcile.groovy | 153 ++++++++++
 .../test_vcg_warmup_restart_master_fe.groovy       | 140 ++++++++++
 .../test_vcg_warmup_restart_target_be.groovy       | 130 +++++++++
 ...vcg_warmup_shared_compute_group_conflict.groovy |  99 +++++++
 ...g_warmup_switch_and_drop_cancels_history.groovy | 130 +++++++++
 ...g_warmup_waits_for_existing_cluster_jobs.groovy | 107 +++++++
 ..._warmup_with_manual_once_queue_semantics.groovy | 111 ++++++++
 ...mup_with_manual_periodic_queue_semantics.groovy | 110 ++++++++
 24 files changed, 2503 insertions(+), 57 deletions(-)

diff --git a/docker/runtime/doris-compose/utils.py 
b/docker/runtime/doris-compose/utils.py
index 99912f0c003..0f41728d3f2 100644
--- a/docker/runtime/doris-compose/utils.py
+++ b/docker/runtime/doris-compose/utils.py
@@ -223,8 +223,8 @@ def detect_docker_compose_cmd():
     Priority:
     1. DORIS_DOCKER_COMPOSE_CMD env override
        e.g. "docker compose" or "docker-compose" or "/path/to/docker-compose"
-    2. docker compose v2
-    3. docker-compose v1 / wrapper
+    2. the first compose client that can talk to the current daemon
+       (prefer docker compose v2, then docker-compose v1 / wrapper)
     """
     def _command_available(cmd):
         try:
@@ -238,26 +238,44 @@ def detect_docker_compose_cmd():
         except (subprocess.CalledProcessError, FileNotFoundError):
             return False
 
+    def _compose_usable(cmd):
+        if not _command_available(cmd + ["version"]):
+            return False
+        try:
+            # `ls` talks to the daemon and catches API-version mismatches that
+            # `version` alone misses on mixed docker/compose installations.
+            subprocess.run(
+                cmd + ["ls"],
+                stdout=subprocess.DEVNULL,
+                stderr=subprocess.DEVNULL,
+                check=True,
+            )
+            return True
+        except subprocess.CalledProcessError:
+            return False
+        except FileNotFoundError:
+            return False
+
     override = os.environ.get("DORIS_DOCKER_COMPOSE_CMD")
     if override:
         cmd = shlex.split(override)
-        if not _command_available(cmd + ["version"]):
+        if not _compose_usable(cmd):
             raise RuntimeError(
                 "DORIS_DOCKER_COMPOSE_CMD is set but not usable: 
{}".format(override)
             )
         return cmd
 
-    # Prefer Compose v2.
-    if _command_available(["docker", "compose", "version"]):
+    # Prefer Compose v2 when it can talk to the daemon.
+    if _compose_usable(["docker", "compose"]):
         return ["docker", "compose"]
 
-    # Fallback for old machines.
-    if _command_available(["docker-compose", "version"]):
+    # Fallback for old machines or environments with a newer standalone client.
+    if _compose_usable(["docker-compose"]):
         return ["docker-compose"]
 
     raise RuntimeError(
-        "Neither 'docker compose' nor 'docker-compose' is available. "
-        "Please install Docker Compose v2 plugin or legacy docker-compose."
+        "Neither 'docker compose' nor 'docker-compose' is usable with the 
current "
+        "Docker daemon. Please install a compatible Docker Compose client."
     )
 
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/CacheHotspotManager.java 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/CacheHotspotManager.java
index 9155745a2e1..e35fa2c6b22 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/cloud/CacheHotspotManager.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/cloud/CacheHotspotManager.java
@@ -404,6 +404,12 @@ public class CacheHotspotManager extends MasterDaemon {
                 continue;
             }
             if (isTableLevelLoadEventWarmUpJob(newJob) != 
isTableLevelLoadEventWarmUpJob(existingJob)) {
+                LOG.info("warmup-conflict detect newJobId={} existingJobId={} 
srcCluster={} dstCluster={} "
+                                + "newJobType={} existingJobType={} 
newTableFilter={} existingTableFilter={}",
+                        newJob.getJobId(), existingJob.getJobId(),
+                        newJob.getSrcClusterName(), newJob.getDstClusterName(),
+                        newJob.getJobType(), existingJob.getJobType(),
+                        newJob.getTableFilterExpr(), 
existingJob.getTableFilterExpr());
                 throw buildLoadEventWarmUpConflictException(newJob, 
existingJob);
             }
         }
@@ -507,6 +513,10 @@ public class CacheHotspotManager extends MasterDaemon {
     public boolean tryRegisterRunningJob(CloudWarmUpJob job) {
         if (job.isEventDriven()) {
             // Event-driven jobs do not require registration, always allow
+            LOG.debug("warmup-lock register-skip jobId={} srcCluster={} 
dstCluster={} syncMode={} jobType={} "
+                            + "reason=event-driven",
+                    job.getJobId(), job.getSrcClusterName(), 
job.getDstClusterName(),
+                    job.getSyncMode(), job.getJobType());
             return true;
         }
 
@@ -516,9 +526,16 @@ public class CacheHotspotManager extends MasterDaemon {
         // Try to register the job atomically if absent
         Long existingJobId = clusterToRunningJobId.putIfAbsent(clusterName, 
jobId);
         boolean success = (existingJobId == null) || (existingJobId == jobId);
-        if (!success) {
-            LOG.info("Job {} skipped: waiting for job {} to finish on 
destination cluster {}",
-                    jobId, existingJobId, clusterName);
+        if (success) {
+            LOG.debug("warmup-lock register jobId={} srcCluster={} 
dstCluster={} syncMode={} jobType={} "
+                            + "existingJobId={} registerResult={}",
+                    jobId, job.getSrcClusterName(), clusterName, 
job.getSyncMode(), job.getJobType(),
+                    existingJobId, "success");
+        } else {
+            LOG.info("warmup-lock register jobId={} srcCluster={} 
dstCluster={} syncMode={} jobType={} "
+                            + "existingJobId={} registerResult={}",
+                    jobId, job.getSrcClusterName(), clusterName, 
job.getSyncMode(), job.getJobType(),
+                    existingJobId, "blocked");
         }
         return success;
     }
@@ -539,19 +556,32 @@ public class CacheHotspotManager extends MasterDaemon {
     private boolean deregisterRunningJob(CloudWarmUpJob job) {
         if (job.isEventDriven()) {
             // Event-driven jobs are not registered, so nothing to deregister
+            LOG.debug("warmup-lock deregister-skip jobId={} srcCluster={} 
dstCluster={} syncMode={} jobType={} "
+                            + "reason=event-driven",
+                    job.getJobId(), job.getSrcClusterName(), 
job.getDstClusterName(),
+                    job.getSyncMode(), job.getJobType());
             return true;
         }
 
         String clusterName = job.getDstClusterName();
         long jobId = job.getJobId();
-
-        return clusterToRunningJobId.remove(clusterName, jobId);
+        Long currentRegisteredJobId = clusterToRunningJobId.get(clusterName);
+        boolean removed = clusterToRunningJobId.remove(clusterName, jobId);
+        LOG.info("warmup-lock deregister jobId={} srcCluster={} dstCluster={} 
syncMode={} jobType={} "
+                        + "currentRegisteredJobId={} releaseResult={} 
jobFinalState={}",
+                jobId, job.getSrcClusterName(), clusterName, 
job.getSyncMode(), job.getJobType(),
+                currentRegisteredJobId, removed, job.getJobState());
+        return removed;
     }
 
     public void notifyJobStop(CloudWarmUpJob job) {
         if (job.isOnce() || job.isPeriodic()) {
             this.deregisterRunningJob(job);
         }
+        LOG.info("warmup-lock notify-stop jobId={} srcCluster={} dstCluster={} 
syncMode={} jobType={} "
+                        + "jobState={} isDone={}",
+                job.getJobId(), job.getSrcClusterName(), 
job.getDstClusterName(),
+                job.getSyncMode(), job.getJobType(), job.getJobState(), 
job.isDone());
         if (!job.isDone()) {
             return;
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java
index 1d9ee1a821c..e2f58cc49b0 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java
@@ -926,6 +926,9 @@ public class CloudWarmUpJob implements Writable {
 
         // make sure only one job runs concurrently for one destination cluster
         if (!((CloudEnv) 
Env.getCurrentEnv()).getCacheHotspotMgr().tryRegisterRunningJob(this)) {
+            LOG.debug("warmup-lock pending-blocked jobId={} srcCluster={} 
dstCluster={} syncMode={} jobType={} "
+                            + "state={}",
+                    jobId, srcClusterName, dstClusterName, syncMode, jobType, 
jobState);
             return;
         }
 
@@ -948,7 +951,9 @@ public class CloudWarmUpJob implements Writable {
         MetricRepo.increaseClusterWarmUpJobExecCount(dstClusterName);
         this.jobState = JobState.RUNNING;
         Env.getCurrentEnv().getEditLog().logModifyCloudWarmUpJob(this);
-        LOG.info("transfer cloud warm up job {} state to {}", jobId, 
this.jobState);
+        LOG.info("warmup-lock state-transition jobId={} srcCluster={} 
dstCluster={} syncMode={} jobType={} "
+                        + "fromState=PENDING toState={} totalTablets={}",
+                jobId, srcClusterName, dstClusterName, syncMode, jobType, 
this.jobState, totalTablets);
     }
 
     private List<TJobMeta> buildJobMetas(long beId, long batchId) {
@@ -1123,6 +1128,14 @@ public class CloudWarmUpJob implements Writable {
                         if (this.isPeriodic()) {
                             // wait for next schedule
                             this.jobState = JobState.PENDING;
+                            long nowMs = System.currentTimeMillis();
+                            long nextScheduleTimeMs = startTimeMs + 
syncInterval * 1000;
+                            LOG.debug("warmup-periodic reschedule jobId={} 
srcCluster={} dstCluster={} "
+                                            + "syncIntervalSec={} 
lastStartTimeMs={} lastFinishTimeMs={} "
+                                            + "nextScheduleTimeMs={} nowMs={} 
triggerImmediately={}",
+                                    jobId, srcClusterName, dstClusterName, 
syncInterval,
+                                    startTimeMs, finishedTimeMs, 
nextScheduleTimeMs, nowMs,
+                                    nowMs >= nextScheduleTimeMs);
                         } else {
                             // release job
                             this.jobState = JobState.FINISHED;
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 4b3b7cf79b7..73805a950ce 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
@@ -205,6 +205,11 @@ public class CloudInstanceStatusChecker extends 
MasterDaemon {
 
     private void cancelCacheJobs(ComputeGroup vcgInFe, List<String> jobIds) {
         CacheHotspotManager cacheHotspotManager = ((CloudEnv) 
Env.getCurrentEnv()).getCacheHotspotMgr();
+        if (!jobIds.isEmpty()) {
+            LOG.info("warmup-vcg cancel-cache-jobs vcgName={} 
activeComputeGroup={} standbyComputeGroup={} "
+                            + "jobIds={}",
+                    vcgInFe.getName(), vcgInFe.getActiveComputeGroup(), 
vcgInFe.getStandbyComputeGroup(), jobIds);
+        }
         for (String jobId : jobIds) {
             try {
                 if (Env.getCurrentEnv().isMaster()) {
@@ -285,6 +290,10 @@ public class CloudInstanceStatusChecker extends 
MasterDaemon {
             String srcCg = virtualGroupInFe.getActiveComputeGroup();
             String dstCg = virtualGroupInFe.getStandbyComputeGroup();
             try {
+                LOG.info("warmup-vcg rebuild-start vcgName={} srcCluster={} 
dstCluster={} subComputeGroups={} "
+                                + "oldJobIds={}",
+                        virtualGroupInFe.getName(), srcCg, dstCg, 
virtualGroupInFe.getSubComputeGroups(),
+                        jobIdsInMs);
                 
cacheHotspotManager.cancelTableLevelLoadEventWarmUpJobsForVirtualComputeGroup(
                         virtualGroupInFe.getName(), srcCg, dstCg, 
virtualGroupInFe.getSubComputeGroups(),
                         "vcg cancel table-level load-event warm up job before 
rebuilding file cache jobs");
@@ -317,11 +326,13 @@ public class CloudInstanceStatusChecker extends 
MasterDaemon {
                 // send jobIds to ms
                 List<String> newJobIds = 
Arrays.asList(Long.toString(jobIdPeriodic), Long.toString(jobIdEvent));
                 CloudSystemInfoService.updateFileCacheJobIds(virtualGroupInFe, 
newJobIds);
-                LOG.info("virtual compute group {}, generate new jobIds 
periodic={}, event={}, and old jobIds {}",
-                        virtualGroupInFe, jobIdPeriodic, jobIdEvent, 
jobIdsInMs);
+                LOG.info("warmup-vcg rebuild-finish vcgName={} srcCluster={} 
dstCluster={} "
+                                + "createdPeriodicJobId={} 
createdEventJobId={} oldJobIds={}",
+                        virtualGroupInFe.getName(), srcCg, dstCg, 
jobIdPeriodic, jobIdEvent, jobIdsInMs);
             } catch (AnalysisException e) {
-                LOG.warn("virtual compute err, name: {}, failed to generate 
file cache warm up jobs: {}",
-                        virtualGroupInFe.getName(), e.getMessage(), e);
+                LOG.warn("warmup-vcg rebuild-failed vcgName={} srcCluster={} 
dstCluster={} oldJobIds={} "
+                                + "failureReason={}",
+                        virtualGroupInFe.getName(), srcCg, dstCg, jobIdsInMs, 
e.getMessage(), e);
                 return;
             }
             virtualGroupInFe.setNeedRebuildFileCache(false);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/CacheHotspotManagerTableFilterTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/CacheHotspotManagerTableFilterTest.java
index 255268e66c5..1b1742ece8a 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/CacheHotspotManagerTableFilterTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/CacheHotspotManagerTableFilterTest.java
@@ -567,6 +567,35 @@ public class CacheHotspotManagerTableFilterTest {
         Assertions.assertEquals(1, manager.getAllJobInfos(10).size());
     }
 
+    @Test
+    public void 
testCreateJobRejectsClusterLevelWhenTableLevelLoadEventExistsAndLogsConflict() 
throws Exception {
+        databases.add(mockDb("ods", mockTable(1001, "orders")));
+
+        WarmUpClusterCommand tableLevel = buildEventDrivenStmt("write_cg", 
"read_cg",
+                new TableFilterRule(RuleType.INCLUDE, "ods.*"));
+        WarmUpClusterCommand clusterLevel = buildEventDrivenStmt("write_cg", 
"read_cg");
+
+        long tableJobId = manager.createJob(tableLevel);
+        RecordingAppender appender = new 
RecordingAppender("warmup-conflict-log-test");
+        Logger logger = (Logger) 
LogManager.getLogger(CacheHotspotManager.class);
+        appender.start();
+        logger.addAppender(appender);
+        try {
+            AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                    () -> manager.createJob(clusterLevel));
+            Assertions.assertTrue(exception.getMessage().contains("Cannot 
create cluster-level load-event warm up job"));
+        } finally {
+            logger.removeAppender(appender);
+            appender.stop();
+        }
+
+        String logs = appender.messagesAsString();
+        Assertions.assertTrue(logs.contains("warmup-conflict detect"), logs);
+        Assertions.assertTrue(logs.contains("existingJobId=" + tableJobId), 
logs);
+        Assertions.assertTrue(logs.contains("srcCluster=write_cg"), logs);
+        Assertions.assertTrue(logs.contains("dstCluster=read_cg"), logs);
+    }
+
     @Test
     public void testVirtualComputeGroupCancelsExistingTableLevelLoadEvent() 
throws Exception {
         databases.add(mockDb("ods", mockTable(1001, "orders")));
@@ -664,6 +693,38 @@ public class CacheHotspotManagerTableFilterTest {
         
Assertions.assertFalse(cancelReasons.containsKey(finishedJob.getJobId()));
     }
 
+    @Test
+    public void testCancelTableFilterJobsForClusterChangeLogsAffectedJobs() 
throws Exception {
+        databases.add(mockDb("ods", mockTable(1001, "orders")));
+
+        CloudWarmUpJob srcMatchedJob = createEventDrivenJob("write_cg", 
"read_cg",
+                new TableFilterRule(RuleType.INCLUDE, "ods.*"));
+        CloudWarmUpJob dstMatchedJob = createEventDrivenJob("other_write_cg", 
"write_cg",
+                new TableFilterRule(RuleType.INCLUDE, "ods.*"));
+        createEventDrivenJob("other_write_cg", "other_read_cg",
+                new TableFilterRule(RuleType.INCLUDE, "ods.*"));
+
+        CacheHotspotManager spyManager = Mockito.spy(manager);
+        Mockito.doAnswer(invocation -> 
null).when(spyManager).cancel(Mockito.anyLong(), Mockito.anyString());
+
+        RecordingAppender appender = new 
RecordingAppender("warmup-system-cancel-log-test");
+        Logger logger = (Logger) 
LogManager.getLogger(CacheHotspotManager.class);
+        appender.start();
+        logger.addAppender(appender);
+        try {
+            spyManager.cancelTableFilterJobsForClusterChange("write_cg",
+                    "system cancel: compute group write_cg renamed to 
write_cg_new");
+        } finally {
+            logger.removeAppender(appender);
+            appender.stop();
+        }
+
+        String logs = appender.messagesAsString();
+        Assertions.assertTrue(logs.contains("write_cg"), logs);
+        
Assertions.assertTrue(logs.contains(String.valueOf(srcMatchedJob.getJobId())), 
logs);
+        
Assertions.assertTrue(logs.contains(String.valueOf(dstMatchedJob.getJobId())), 
logs);
+    }
+
     // ===== Async materialized view (MTMV) matching =====
 
     @Test
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/cache/CacheHotspotManagerTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/cache/CacheHotspotManagerTest.java
index e25eb98d140..e921f5d079f 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/cache/CacheHotspotManagerTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/cache/CacheHotspotManagerTest.java
@@ -37,6 +37,11 @@ import 
org.apache.doris.nereids.trees.plans.commands.WarmUpClusterCommand;
 import org.apache.doris.persist.EditLog;
 import org.apache.doris.system.Backend;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -340,6 +345,30 @@ public class CacheHotspotManagerTest {
         Assert.assertEquals(1, 
cacheHotspotManager.getCloudWarmUpJobs().size());
     }
 
+    @Test
+    public void testTryRegisterRunningJobLogsBlockedResult() {
+        CloudWarmUpJob runningJob = newClusterJob(10L, "src_a", "dst_a", 
SyncMode.ONCE, JobState.RUNNING, 100L);
+        CloudWarmUpJob blockedJob = newClusterJob(11L, "src_b", "dst_a", 
SyncMode.ONCE, JobState.PENDING, 200L);
+
+        RecordingAppender appender = new 
RecordingAppender("warmup-lock-register-log-test");
+        Logger logger = (Logger) 
LogManager.getLogger(CacheHotspotManager.class);
+        appender.start();
+        logger.addAppender(appender);
+        try {
+            
Assert.assertTrue(cacheHotspotManager.tryRegisterRunningJob(runningJob));
+            
Assert.assertFalse(cacheHotspotManager.tryRegisterRunningJob(blockedJob));
+        } finally {
+            logger.removeAppender(appender);
+            appender.stop();
+        }
+
+        String logs = appender.messagesAsString();
+        Assert.assertTrue(logs, logs.contains("warmup-lock register"));
+        Assert.assertTrue(logs, logs.contains("jobId=11"));
+        Assert.assertTrue(logs, logs.contains("existingJobId=10"));
+        Assert.assertTrue(logs, logs.contains("registerResult=blocked"));
+    }
+
     private WarmUpClusterCommand newTableStmt(String dstClusterName, boolean 
force,
             Triple<String, String, String>... tables) {
         WarmUpClusterCommand stmt = new WarmUpClusterCommand(new ArrayList<>(),
@@ -429,4 +458,21 @@ public class CacheHotspotManagerTest {
         }
         Assert.fail("Timed out waiting for once pending create lock ref count 
" + expectedRefCount);
     }
+
+    private static class RecordingAppender extends AbstractAppender {
+        private final List<String> messages = new ArrayList<>();
+
+        RecordingAppender(String name) {
+            super(name, null, null, true, Property.EMPTY_ARRAY);
+        }
+
+        @Override
+        public void append(LogEvent event) {
+            messages.add(event.getMessage().getFormattedMessage());
+        }
+
+        String messagesAsString() {
+            return String.join("\n", messages);
+        }
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudInstanceStatusCheckerTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudInstanceStatusCheckerTest.java
index ff19f67dcb6..95eb739de64 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudInstanceStatusCheckerTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/cloud/catalog/CloudInstanceStatusCheckerTest.java
@@ -166,7 +166,10 @@ public class CloudInstanceStatusCheckerTest {
 
         String logs = appender.messagesAsString();
         Assertions.assertFalse(logs.contains("failed to create virtual compute 
group vcg"), logs);
-        Assertions.assertTrue(logs.contains("generate new jobIds"), logs);
+        Assertions.assertTrue(logs.contains("warmup-vcg rebuild-start"), logs);
+        Assertions.assertTrue(logs.contains("warmup-vcg rebuild-finish"), 
logs);
+        Assertions.assertTrue(logs.contains("srcCluster=active_cg"), logs);
+        Assertions.assertTrue(logs.contains("dstCluster=standby_cg"), logs);
     }
 
     private void addComputeGroup(String computeGroupId, String 
computeGroupName) {
diff --git 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy
 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy
index aa877adb413..846ce4ac34b 100644
--- 
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy
+++ 
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy
@@ -167,9 +167,9 @@ class WarmupMetricsUtils {
             }
             Thread.sleep(2000)
         }
-        logger.warn("waitForWarmupFinish timed out after ${timeoutMs}ms, " +
-                "expected finished >= ${expectedFinished}")
-        return getWarmupMetrics(sqlRunner, srcCluster, dstCluster)
+        def latest = getWarmupMetrics(sqlRunner, srcCluster, dstCluster)
+        throw new RuntimeException("waitForWarmupFinish timed out after 
${timeoutMs}ms, "
+                + "expected finished >= ${expectedFinished}, latest=${latest}")
     }
 
     /**
@@ -265,4 +265,308 @@ class WarmupMetricsUtils {
         logger.warn("waitForMetricsStable timed out after ${timeoutMs}ms")
         return getWarmupMetrics(sqlRunner, srcCluster, dstCluster)
     }
+
+    static List<Map> showWarmupJobs(Closure sqlRunner) {
+        def rows = sqlRunner("SHOW WARM UP JOB")
+        return rows.collect { row -> normalizeWarmupJobRow(row) }
+    }
+
+    static Map showWarmupJob(Closure sqlRunner, Object jobId) {
+        def rows = sqlRunner("SHOW WARM UP JOB WHERE ID = ${jobId}")
+        if (rows == null || rows.isEmpty()) {
+            throw new RuntimeException("warmup job ${jobId} not found")
+        }
+        return normalizeWarmupJobRow(rows[0])
+    }
+
+    static Map normalizeWarmupJobRow(Object row) {
+        List values = row as List
+        return [
+                jobId          : values[0]?.toString(),
+                srcCluster     : values[1]?.toString(),
+                dstCluster     : values[2]?.toString(),
+                status         : values[3]?.toString(),
+                type           : values[4]?.toString(),
+                syncMode       : values[5]?.toString(),
+                createTime     : values[6]?.toString(),
+                startTime      : values[7]?.toString(),
+                finishedTime   : values[8]?.toString(),
+                allBatch       : values[9]?.toString(),
+                finishedBatch  : values[10]?.toString(),
+                errMsg         : values[11]?.toString(),
+                tableOrPattern : values.size() > 12 ? values[12]?.toString() : 
"",
+                tableFilter    : values.size() > 13 ? values[13]?.toString() : 
"",
+                matchedTables  : values.size() > 14 ? values[14]?.toString() : 
"",
+                syncStats      : values.size() > 15 ? values[15]?.toString() : 
"",
+                raw            : values,
+        ]
+    }
+
+    static boolean isNormalWarmupJob(Map row) {
+        String syncMode = row.syncMode ?: ""
+        return syncMode.startsWith("ONCE") || syncMode.startsWith("PERIODIC")
+    }
+
+    static boolean isEventDrivenWarmupJob(Map row) {
+        String syncMode = row.syncMode ?: ""
+        return syncMode.startsWith("EVENT_DRIVEN")
+    }
+
+    static boolean isActiveWarmupJob(Map row) {
+        return row.status in ["PENDING", "RUNNING", "WAITING"]
+    }
+
+    static boolean isClusterWarmupJob(Map row) {
+        return row.type == "CLUSTER"
+    }
+
+    static List<Map> showWarmupJobsByDst(Closure sqlRunner, String dstCluster) 
{
+        return showWarmupJobs(sqlRunner).findAll { it.dstCluster == dstCluster 
}
+    }
+
+    static long countRunningNormalWarmupByDst(Closure sqlRunner, String 
dstCluster) {
+        return showWarmupJobsByDst(sqlRunner, dstCluster).count {
+            isNormalWarmupJob(it) && it.status == "RUNNING"
+        } as long
+    }
+
+    static List<Map> snapshotWarmupJobs(Closure sqlRunner) {
+        return showWarmupJobs(sqlRunner).collect { new LinkedHashMap<>(it) }
+    }
+
+    static Map<String, Map> snapshotWarmupJobsById(Closure sqlRunner) {
+        Map<String, Map> result = [:]
+        snapshotWarmupJobs(sqlRunner).each { result[it.jobId] = it }
+        return result
+    }
+
+    static void waitForOnlyOneRunningNormalWarmup(Closure sqlRunner, String 
dstCluster,
+                                                  long timeoutMs = 30000,
+                                                  Collection<String> 
expectedJobIds = null,
+                                                  long observeMs = 5000) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        List<Map> lastRunningRows = []
+        while (System.currentTimeMillis() < deadline) {
+            List<Map> runningRows = showWarmupJobsByDst(sqlRunner, 
dstCluster).findAll {
+                isNormalWarmupJob(it) && it.status == "RUNNING"
+            }
+            lastRunningRows = runningRows
+            if (runningRows.size() > 1) {
+                throw new RuntimeException("expected at most one running 
normal warmup on ${dstCluster}, "
+                        + "runningRows=${runningRows}")
+            }
+            boolean hasExpectedRunning = expectedJobIds == null || 
runningRows.any {
+                expectedJobIds.contains(it.jobId)
+            }
+            if (runningRows.size() == 1 && hasExpectedRunning) {
+                assertAtMostOneRunningNormalWarmupDuring(sqlRunner, 
dstCluster, observeMs)
+                return
+            }
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException("expected one normal warmup to reach 
RUNNING on ${dstCluster}, "
+                + "expectedJobIds=${expectedJobIds}, 
lastRunningRows=${lastRunningRows}")
+    }
+
+    static void assertAtMostOneRunningNormalWarmupDuring(Closure sqlRunner, 
String dstCluster, long observeMs) {
+        long deadline = System.currentTimeMillis() + observeMs
+        while (System.currentTimeMillis() < deadline) {
+            List<Map> runningRows = showWarmupJobsByDst(sqlRunner, 
dstCluster).findAll {
+                isNormalWarmupJob(it) && it.status == "RUNNING"
+            }
+            if (runningRows.size() > 1) {
+                throw new RuntimeException("expected at most one running 
normal warmup on ${dstCluster}, "
+                        + "runningRows=${runningRows}")
+            }
+            Thread.sleep(1000)
+        }
+    }
+
+    static Map waitForWarmupJobsRecovered(Closure sqlRunner, Map<String, Map> 
beforeSnapshot,
+                                          Closure<Boolean> predicate, long 
timeoutMs = 60000) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        Map<String, Map> current = [:]
+        while (System.currentTimeMillis() < deadline) {
+            current = snapshotWarmupJobsById(sqlRunner)
+            if (predicate(beforeSnapshot, current)) {
+                return current
+            }
+            Thread.sleep(1000)
+        }
+        return current
+    }
+
+    static Map waitForPeriodicCycles(Closure sqlRunner, Object jobId, int 
minTransitions,
+                                     long timeoutMs = 120000) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        List<String> states = []
+        String lastState = null
+        while (System.currentTimeMillis() < deadline) {
+            def row = showWarmupJob(sqlRunner, jobId)
+            if (row.status != lastState) {
+                states << row.status
+                lastState = row.status
+            }
+            int transitions = 0
+            for (int i = 1; i < states.size(); i++) {
+                if (states[i - 1] == "RUNNING" && (states[i] == "PENDING" || 
states[i] == "WAITING")) {
+                    transitions++
+                }
+            }
+            if (transitions >= minTransitions) {
+                return [states: states, row: row]
+            }
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException("expected periodic job ${jobId} to observe 
${minTransitions} "
+                + "RUNNING-to-waiting transitions within ${timeoutMs}ms, 
states=${states}")
+    }
+
+    static List<Map> sampleJobTimeline(Closure sqlRunner, Object jobId, long 
durationMs,
+                                       long intervalMs = 1000) {
+        long deadline = System.currentTimeMillis() + durationMs
+        List<Map> samples = []
+        while (System.currentTimeMillis() < deadline) {
+            def row = showWarmupJob(sqlRunner, jobId)
+            samples << [
+                    ts      : System.currentTimeMillis(),
+                    status  : row.status,
+                    start   : row.startTime,
+                    finished: row.finishedTime,
+                    syncMode: row.syncMode,
+            ]
+            Thread.sleep(intervalMs)
+        }
+        return samples
+    }
+
+    static List<Map> collectWarmupJobsByCluster(Closure sqlRunner, String 
clusterName) {
+        return showWarmupJobs(sqlRunner).findAll {
+            it.srcCluster == clusterName || it.dstCluster == clusterName
+        }
+    }
+
+    static void assertAffectedJobsCancelled(Closure sqlRunner, 
Collection<String> jobIds,
+                                            long timeoutMs = 60000) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        Map<String, String> lastStates = [:]
+        while (System.currentTimeMillis() < deadline) {
+            boolean allCancelled = true
+            for (String jobId in jobIds) {
+                def row = showWarmupJob(sqlRunner, jobId)
+                lastStates[jobId] = row.status
+                if (row.status != "CANCELLED") {
+                    allCancelled = false
+                }
+            }
+            if (allCancelled) {
+                return
+            }
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException("expected jobs cancelled, 
lastStates=${lastStates}")
+    }
+
+    static void assertUnrelatedJobsUnaffected(Closure sqlRunner, 
Collection<String> jobIds,
+                                              Collection<String> 
allowedStatuses = ["RUNNING", "PENDING", "FINISHED"]) {
+        for (String jobId in jobIds) {
+            def row = showWarmupJob(sqlRunner, jobId)
+            if (!allowedStatuses.contains(row.status)) {
+                throw new RuntimeException("unrelated warmup job ${jobId} 
entered unexpected status ${row.status}")
+            }
+        }
+    }
+
+    static String expectCreateConflict(Closure sqlRunner, Closure createJob) {
+        try {
+            createJob.call()
+        } catch (Throwable t) {
+            return t.message ?: t.toString()
+        }
+        throw new RuntimeException("expected warmup create conflict, but 
statement succeeded")
+    }
+
+    static void assertConflictMessage(String message, Collection<String> 
expectedKeywords) {
+        expectedKeywords.each { keyword ->
+            if (!(message?.toLowerCase()?.contains(keyword.toLowerCase()))) {
+                throw new RuntimeException("expected conflict message to 
contain '${keyword}', actual=${message}")
+            }
+        }
+    }
+
+    static List<Map> getVcgWarmupJobs(Closure sqlRunner, String srcCluster, 
String dstCluster) {
+        return showWarmupJobs(sqlRunner).findAll {
+            it.srcCluster == srcCluster && it.dstCluster == dstCluster
+                    && isActiveWarmupJob(it) && isClusterWarmupJob(it)
+                    && (it.syncMode.startsWith("PERIODIC") || 
it.syncMode.startsWith("EVENT_DRIVEN"))
+        }
+    }
+
+    static List<String> getVcgWarmupJobIds(Closure sqlRunner, String 
srcCluster, String dstCluster) {
+        return getVcgWarmupJobs(sqlRunner, srcCluster, dstCluster).collect { 
it.jobId }
+    }
+
+    static boolean hasPeriodicAndEventDrivenWarmup(Collection<Map> rows) {
+        return rows.any { it.syncMode.startsWith("PERIODIC") }
+                && rows.any { it.syncMode.startsWith("EVENT_DRIVEN") }
+    }
+
+    static List<Map> waitForWarmupJobsByPair(Closure sqlRunner, String 
srcCluster, String dstCluster,
+                                             int minCount = 1, long timeoutMs 
= 120000) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        List<Map> rows = []
+        while (System.currentTimeMillis() < deadline) {
+            rows = showWarmupJobs(sqlRunner).findAll {
+                it.srcCluster == srcCluster && it.dstCluster == dstCluster
+            }
+            if (rows.size() >= minCount) {
+                return rows
+            }
+            Thread.sleep(1000)
+        }
+        return rows
+    }
+
+    static List<String> waitForVcgWarmupRecreated(Closure sqlRunner, String 
srcCluster, String dstCluster,
+                                                  Collection<String> 
oldJobIds, int minNewJobs = 1,
+                                                  long timeoutMs = 120000) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        List<Map> newRows = []
+        while (System.currentTimeMillis() < deadline) {
+            newRows = getVcgWarmupJobs(sqlRunner, srcCluster, 
dstCluster).findAll {
+                !oldJobIds.contains(it.jobId)
+            }
+            if (newRows.size() >= minNewJobs && 
hasPeriodicAndEventDrivenWarmup(newRows)) {
+                return newRows.collect { it.jobId }
+            }
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException("expected active VCG cluster warmup jobs 
recreated for "
+                + "${srcCluster}->${dstCluster}, oldJobIds=${oldJobIds}, 
lastRows=${newRows}")
+    }
+
+    static void assertHistoricalJobsCancelled(Closure sqlRunner, 
Collection<String> jobIds,
+                                              long timeoutMs = 120000) {
+        assertAffectedJobsCancelled(sqlRunner, jobIds.collect { it.toString() 
}, timeoutMs)
+    }
+
+    static Map waitForWarmupStatsResume(Closure sqlRunner, Object jobId, 
Closure<Boolean> predicate,
+                                        long timeoutMs = 120000) {
+        return waitForJobSyncStats(sqlRunner, jobId, predicate, timeoutMs)
+    }
+
+    static void waitForJobStatus(Closure sqlRunner, Object jobId, 
Collection<String> expectedStatuses,
+                                 long timeoutMs = 60000) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        String lastStatus = null
+        while (System.currentTimeMillis() < deadline) {
+            def row = showWarmupJob(sqlRunner, jobId)
+            lastStatus = row.status
+            if (expectedStatuses.contains(lastStatus)) {
+                return
+            }
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException("warmup job ${jobId} status ${lastStatus} 
not in ${expectedStatuses}")
+    }
 }
diff --git 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_event_conflict_and_diagnostics.groovy
 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_event_conflict_and_diagnostics.groovy
new file mode 100644
index 00000000000..92725326d4b
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_event_conflict_and_diagnostics.groovy
@@ -0,0 +1,111 @@
+// 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 org.apache.doris.regression.suite.ClusterOptions
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_warm_up_cluster_event_conflict_and_diagnostics', 'docker') {
+    def options = new ClusterOptions()
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'cloud_warm_up_table_filter_refresh_interval_ms=1000',
+    ]
+    options.beConfigs += [
+        'file_cache_enter_disk_resource_limit_mode_percent=99',
+        'enable_evict_file_cache_in_advance=false',
+        'file_cache_background_monitor_interval_ms=1000',
+    ]
+    options.cloudMode = true
+    options.beNum = 1
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def srcCluster = "warmup_conflict_src"
+        def dstCluster = "warmup_conflict_dst"
+        def dbName = "test_warmup_conflict_db"
+        def tableName = "conflict_tbl"
+        def jobIds = []
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+
+        sql """use @${srcCluster}"""
+        sql """CREATE DATABASE IF NOT EXISTS ${dbName}"""
+        sql """use ${dbName}"""
+        sql """DROP TABLE IF EXISTS ${tableName}"""
+        sql """CREATE TABLE ${tableName} (
+                   id INT,
+                   value STRING
+               )
+               DUPLICATE KEY(id)
+               DISTRIBUTED BY HASH(id) BUCKETS 1
+               PROPERTIES ("file_cache_ttl_seconds" = "3600")"""
+
+        def clusterJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+        jobIds << clusterJobId
+
+        String clusterFirstConflict = 
WarmupMetricsUtils.expectCreateConflict(sqlRunner, {
+            sql("""
+                WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+                ON TABLES (INCLUDE '${dbName}.${tableName}')
+                PROPERTIES ("sync_mode" = "event_driven", "sync_event" = 
"load")
+            """)
+        })
+        WarmupMetricsUtils.assertConflictMessage(clusterFirstConflict, [
+                "conflicting", "cluster-level", "table-level", srcCluster, 
dstCluster
+        ])
+
+        sql """CANCEL WARM UP JOB WHERE ID = ${clusterJobId}"""
+        def cancelledCluster = WarmupMetricsUtils.showWarmupJob(sqlRunner, 
clusterJobId)
+        assertEquals("CANCELLED", cancelledCluster.status)
+
+        def tableJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            ON TABLES (INCLUDE '${dbName}.${tableName}')
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+        jobIds << tableJobId
+        assert WarmupMetricsUtils.waitForMatchedTables(sqlRunner, tableJobId,
+                ["${dbName}.${tableName}".toString()] as Set) ==
+                ["${dbName}.${tableName}".toString()] as Set
+
+        String tableFirstConflict = 
WarmupMetricsUtils.expectCreateConflict(sqlRunner, {
+            sql("""
+                WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+                PROPERTIES ("sync_mode" = "event_driven", "sync_event" = 
"load")
+            """)
+        })
+        WarmupMetricsUtils.assertConflictMessage(tableFirstConflict, [
+                "conflicting", "table-level", "cluster-level", srcCluster, 
dstCluster
+        ])
+
+        sql """CANCEL WARM UP JOB WHERE ID = ${tableJobId}"""
+        def cancelledTable = WarmupMetricsUtils.showWarmupJob(sqlRunner, 
tableJobId)
+        assertEquals("CANCELLED", cancelledTable.status)
+
+        jobIds.each { jid ->
+            try {
+                sql """CANCEL WARM UP JOB WHERE ID = ${jid}"""
+            } catch (Throwable ignored) {
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_periodic_add_new_be.groovy
 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_periodic_add_new_be.groovy
index fa476e80ba6..d0f38a024d4 100644
--- 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_periodic_add_new_be.groovy
+++ 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_periodic_add_new_be.groovy
@@ -84,11 +84,73 @@ suite('test_warm_up_cluster_periodic_add_new_be', 'docker') 
{
         }
     }
 
-    def getTTLCacheSize = { ip, port ->
-        return getBrpcMetrics(ip, port, "ttl_cache_size")
+    def logFileCacheQueueMetrics = { cluster, phase ->
+        def queueMetrics = [
+                normal    : [
+                        size : "file_cache_normal_queue_cache_size",
+                        count: "file_cache_normal_queue_element_count",
+                        evict: "file_cache_normal_queue_evict_size",
+                ],
+                index     : [
+                        size : "file_cache_index_queue_cache_size",
+                        count: "file_cache_index_queue_element_count",
+                        evict: "file_cache_index_queue_evict_size",
+                ],
+                disposable: [
+                        size : "file_cache_disposable_queue_cache_size",
+                        count: "file_cache_disposable_queue_element_count",
+                        evict: "file_cache_disposable_queue_evict_size",
+                ],
+                ttl       : [
+                        size : "file_cache_ttl_cache_lru_queue_size",
+                        count: "file_cache_ttl_cache_lru_queue_element_count",
+                        evict: "file_cache_ttl_cache_evict_size",
+                ],
+        ]
+
+        def backends = sql """SHOW BACKENDS"""
+        def cluster_bes = backends.findAll { 
it[19].contains("""\"compute_group_name\" : \"${cluster}\"""") }
+        for (be in cluster_bes) {
+            def ip = be[1]
+            def port = be[5]
+            def fileCacheSize = getBrpcMetrics(ip, port, 
"file_cache_cache_size")
+            def queueMetricDetails = []
+            queueMetrics.each { queueName, metrics ->
+                def size = getBrpcMetrics(ip, port, metrics.size)
+                def count = getBrpcMetrics(ip, port, metrics.count)
+                def evict = getBrpcMetrics(ip, port, metrics.evict)
+                queueMetricDetails.add("${queueName}_queue{size=${size}, 
element_count=${count}, evict_size=${evict}}")
+            }
+            logger.info("${phase} ${cluster} be ${ip}:${port} 
file_cache_size=${fileCacheSize}, "
+                    + "file_cache_queue_metrics: ${queueMetricDetails.join(', 
')}")
+        }
     }
 
-    def getClusterTTLCacheSizeSum = { cluster ->
+    def logFileCacheQueueCacheSizeMetrics = { cluster, phase ->
+        def queueCacheSizeMetrics = [
+                normal    : "file_cache_normal_queue_cache_size",
+                index     : "file_cache_index_queue_cache_size",
+                disposable: "file_cache_disposable_queue_cache_size",
+                ttl       : "file_cache_ttl_cache_lru_queue_size",
+        ]
+
+        def backends = sql """SHOW BACKENDS"""
+        def cluster_bes = backends.findAll { 
it[19].contains("""\"compute_group_name\" : \"${cluster}\"""") }
+        for (be in cluster_bes) {
+            def ip = be[1]
+            def port = be[5]
+            queueCacheSizeMetrics.each { queueName, metricName ->
+                def size = getBrpcMetrics(ip, port, metricName)
+                logger.info("${phase} ${cluster} be ${ip}:${port} 
${queueName}_queue cache_size=${size}")
+            }
+        }
+    }
+
+    def getFileCacheSize = { ip, port ->
+        return getBrpcMetrics(ip, port, "file_cache_cache_size")
+    }
+
+    def getClusterFileCacheSizeSum = { cluster ->
         def backends = sql """SHOW BACKENDS"""
 
         def cluster_bes = backends.findAll { 
it[19].contains("""\"compute_group_name\" : \"${cluster}\"""") }
@@ -97,44 +159,79 @@ suite('test_warm_up_cluster_periodic_add_new_be', 
'docker') {
         for (be in cluster_bes) {
             def ip = be[1]
             def port = be[5]
-            def size = getTTLCacheSize(ip, port)
+            def size = getFileCacheSize(ip, port)
             sum += size
-            logger.info("be be ${ip}:${port} ttl cache size ${size}")
+            logger.info("${cluster} be ${ip}:${port} file cache size ${size}")
         }
 
         return sum
     }
 
-    def checkTTLCacheSizeSumEqual = { cluster1, cluster2 ->
+    def checkFileCacheSizeSumEqual = { cluster1, cluster2 ->
+        def srcSum = getClusterFileCacheSizeSum(cluster1)
+        def dstSum = getClusterFileCacheSizeSum(cluster2)
+
+        logger.info("file_cache_cache_size: src=${srcSum} dst=${dstSum}")
+        assertTrue(srcSum > 0, "file_cache_cache_size should > 0")
+        assertEquals(srcSum, dstSum)
+    }
+
+    def getClusterMetricSum = { cluster, metricName ->
         def backends = sql """SHOW BACKENDS"""
 
-        def srcBes = backends.findAll { 
it[19].contains("""\"compute_group_name\" : \"${cluster1}\"""") }
-        def tgtBes = backends.findAll { 
it[19].contains("""\"compute_group_name\" : \"${cluster2}\"""") }
+        def cluster_bes = backends.findAll { 
it[19].contains("""\"compute_group_name\" : \"${cluster}\"""") }
 
-        long srcSum = 0
-        for (src in srcBes) {
-            def ip = src[1]
-            def port = src[5]
-            def size = getTTLCacheSize(ip, port)
-            srcSum += size
-            logger.info("src be ${ip}:${port} ttl cache size ${size}")
+        long sum = 0
+        for (be in cluster_bes) {
+            def ip = be[1]
+            def port = be[5]
+            def value = getBrpcMetrics(ip, port, metricName)
+            sum += value
+            logger.info("${cluster} be ${ip}:${port} ${metricName} ${value}")
         }
 
-        long tgtSum = 0
-        for (tgt in tgtBes) {
-            def ip = tgt[1]
-            def port = tgt[5]
-            def size = getTTLCacheSize(ip, port)
-            tgtSum += size
-            logger.info("dst be ${ip}:${port} ttl cache size ${size}")
+        return sum
+    }
+
+    def getPeriodicWarmupMetrics = { cluster ->
+        [
+                submitted: getClusterMetricSum(cluster, 
"file_cache_once_or_periodic_warm_up_submitted_segment_num"),
+                finished : getClusterMetricSum(cluster, 
"file_cache_once_or_periodic_warm_up_finished_segment_num"),
+        ]
+    }
+
+    def loadCustomerData = { clusterName, tableName, rowCount ->
+        sql """use @${clusterName}"""
+
+        def dataFile = 
File.createTempFile("test_warm_up_cluster_periodic_add_new_be_", ".csv")
+        dataFile.deleteOnExit()
+        dataFile.withWriter("UTF-8") { writer ->
+            for (int i = 1; i <= rowCount; i++) {
+                writer.write("${i},name_${i}\n")
+            }
         }
 
-        logger.info("ttl_cache_size: src=${srcSum} dst=${tgtSum}")
-        assertTrue(srcSum > 0, "ttl_cache_size should > 0")
-        assertEquals(srcSum, tgtSum)
+        streamLoad {
+            table tableName
+            set 'column_separator', ','
+            set 'compute_group', clusterName
+            file dataFile.getAbsolutePath()
+            time 60000
+
+            check { result, exception, startTime, endTime ->
+                if (exception != null) {
+                    throw exception
+                }
+                logger.info("Stream load result: ${result}".toString())
+                def json = parseJson(result)
+                assertEquals("success", json.Status.toLowerCase())
+                assertEquals(rowCount, json.NumberTotalRows)
+                assertEquals(rowCount, json.NumberLoadedRows)
+            }
+        }
     }
 
-    def waitUntil = { condition, timeoutMs ->
+    def waitUntil = { condition, timeoutMs, description ->
         long start = System.currentTimeMillis()
         while (System.currentTimeMillis() - start < timeoutMs) {
             if (condition()) {
@@ -142,6 +239,7 @@ suite('test_warm_up_cluster_periodic_add_new_be', 'docker') 
{
             }
             sleep(1000)
         }
+        throw new RuntimeException("Timed out after ${timeoutMs}ms waiting for 
${description}")
     }
 
     docker(options) {
@@ -172,7 +270,9 @@ suite('test_warm_up_cluster_periodic_add_new_be', 'docker') 
{
 
         // Simple setup to simulate data load and access
         sql """CREATE TABLE IF NOT EXISTS customer (id INT, name STRING) 
DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 3 PROPERTIES 
("file_cache_ttl_seconds" = "3600")"""
-        sql """INSERT INTO customer VALUES (1, 'A'), (2, 'B'), (3, 'C')"""
+        loadCustomerData(clusterName1, "customer", 3000)
+        logFileCacheQueueCacheSizeMetrics(clusterName1, "after load before 
clear cache")
+        logFileCacheQueueCacheSizeMetrics(clusterName2, "after load before 
clear cache")
 
         // Start warm up job
         def jobId_ = sql """
@@ -193,19 +293,35 @@ suite('test_warm_up_cluster_periodic_add_new_be', 
'docker') {
         sql """truncate table __internal_schema.cloud_cache_hotspot;"""
         clearFileCacheOnAllBackends()
 
-        for (int i = 0; i < 1000; i++) {
+        def beforeWarmupMetrics = getPeriodicWarmupMetrics(clusterName2)
+        logFileCacheQueueMetrics(clusterName1, "after clear cache before 
warmup")
+        logFileCacheQueueMetrics(clusterName2, "after clear cache before 
warmup")
+        for (int i = 0; i < 500; i++) {
             sql """SELECT * FROM customer"""
         }
 
-        waitUntil({ getClusterTTLCacheSizeSum(clusterName1) > 0 }, 60000)
-        waitUntil({ getClusterTTLCacheSizeSum(clusterName1) == 
getClusterTTLCacheSizeSum(clusterName2) }, 60000)
+        waitUntil({
+            def metrics = getPeriodicWarmupMetrics(clusterName2)
+            metrics.submitted > beforeWarmupMetrics.submitted && 
metrics.finished > beforeWarmupMetrics.finished
+        }, 120000, "periodic warmup metrics to advance from 
${beforeWarmupMetrics}")
 
-        def hotspot = sql """select * from 
__internal_schema.cloud_cache_hotspot;"""
-        logger.info("hotspot: {}", hotspot)
+        waitUntil({ getClusterFileCacheSizeSum(clusterName1) > 0 },
+                60000, "${clusterName1} file cache size > 0")
+        waitUntil({
+            logFileCacheQueueMetrics(clusterName1, "wait for warmup")
+            logFileCacheQueueMetrics(clusterName2, "wait for warmup")
+            getClusterFileCacheSizeSum(clusterName1) == 
getClusterFileCacheSizeSum(clusterName2)
+        }, 60000, "${clusterName1} and ${clusterName2} file cache size to 
match")
 
         logFileCacheDownloadMetrics(clusterName2)
-        assertTrue(getClusterTTLCacheSizeSum(clusterName1) > 0)
-        checkTTLCacheSizeSumEqual(clusterName1, clusterName2)
+        logFileCacheQueueMetrics(clusterName1, "after warmup")
+        logFileCacheQueueMetrics(clusterName2, "after warmup")
+        def afterWarmupMetrics = getPeriodicWarmupMetrics(clusterName2)
+        assertTrue(afterWarmupMetrics.submitted > 
beforeWarmupMetrics.submitted,
+                "periodic warmup should submit segments, 
before=${beforeWarmupMetrics}, after=${afterWarmupMetrics}")
+        assertTrue(afterWarmupMetrics.finished > beforeWarmupMetrics.finished,
+                "periodic warmup should finish segments, 
before=${beforeWarmupMetrics}, after=${afterWarmupMetrics}")
+        checkFileCacheSizeSumEqual(clusterName1, clusterName2)
 
         def jobInfo = sql """SHOW WARM UP JOB WHERE ID = ${jobId}"""
         assertEquals(jobInfo[0][0], jobId)
diff --git 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_periodic_scheduler_semantics.groovy
 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_periodic_scheduler_semantics.groovy
new file mode 100644
index 00000000000..022a550ff86
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_periodic_scheduler_semantics.groovy
@@ -0,0 +1,137 @@
+// 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 org.apache.doris.regression.suite.ClusterOptions
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_warm_up_cluster_periodic_scheduler_semantics', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.beConfigs += [
+        'file_cache_enter_disk_resource_limit_mode_percent=99',
+        'enable_evict_file_cache_in_advance=false',
+        'file_cache_background_monitor_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+        def srcCluster = "warmup_periodic_src"
+        def dstCluster = "warmup_periodic_dst"
+        def dbName = "test_warmup_periodic_scheduler_db"
+        def tableName = "periodic_tbl"
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+
+        def restartMasterFe = {
+            def oldMasterFe = cluster.getMasterFe()
+            cluster.restartFrontends(oldMasterFe.index)
+            boolean restarted = false
+            for (int i = 0; i < 30; i++) {
+                if (cluster.getFeByIndex(oldMasterFe.index).alive) {
+                    restarted = true
+                    break
+                }
+                sleep(1000)
+            }
+            assertTrue(restarted)
+            context.reconnectFe()
+        }
+
+        def getPeriodicWarmupMetrics = {
+            [
+                    submitted: 
WarmupMetricsUtils.getClusterMetricSum(sqlRunner, dstCluster,
+                            
"file_cache_once_or_periodic_warm_up_submitted_segment_num"),
+                    finished : 
WarmupMetricsUtils.getClusterMetricSum(sqlRunner, dstCluster,
+                            
"file_cache_once_or_periodic_warm_up_finished_segment_num"),
+            ]
+        }
+
+        def waitForPeriodicWarmupFinish = { Map before, long timeoutMs ->
+            long deadline = System.currentTimeMillis() + timeoutMs
+            Map latest = getPeriodicWarmupMetrics()
+            while (System.currentTimeMillis() < deadline) {
+                latest = getPeriodicWarmupMetrics()
+                if (latest.submitted > before.submitted && latest.finished > 
before.finished
+                        && latest.finished >= latest.submitted) {
+                    return latest
+                }
+                sleep(2000)
+            }
+            logger.warn("periodic warmup metrics did not advance after 
${timeoutMs}ms, "
+                    + "before=${before}, latest=${latest}")
+            return latest
+        }
+
+        sql """use @${srcCluster}"""
+        sql """CREATE DATABASE IF NOT EXISTS ${dbName}"""
+        sql """use ${dbName}"""
+        sql """CREATE TABLE IF NOT EXISTS ${tableName} (
+                   id INT,
+                   value STRING
+               )
+               DUPLICATE KEY(id)
+               DISTRIBUTED BY HASH(id) BUCKETS 1
+               PROPERTIES ("file_cache_ttl_seconds" = "3600")"""
+        for (int i = 0; i < 12; i++) {
+            sql """INSERT INTO ${tableName} VALUES (${i}, 'before_${i}')"""
+        }
+
+        def periodicJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            PROPERTIES (
+                "sync_mode" = "periodic",
+                "sync_interval_sec" = "1"
+            )
+        """)[0][0]
+
+        def cycles = WarmupMetricsUtils.waitForPeriodicCycles(sqlRunner, 
periodicJobId, 1, 120000)
+        assertTrue(cycles.states.contains("RUNNING"))
+        assertTrue(cycles.states.any { it in ["PENDING", "WAITING"] })
+
+        def timelineBeforeRestart = 
WarmupMetricsUtils.sampleJobTimeline(sqlRunner, periodicJobId, 5000, 1000)
+        assertTrue(timelineBeforeRestart*.status.any { it in ["RUNNING", 
"PENDING", "WAITING"] })
+
+        restartMasterFe()
+        sql """use @${srcCluster}"""
+        sql """use ${dbName}"""
+
+        def timelineAfterRestart = 
WarmupMetricsUtils.sampleJobTimeline(sqlRunner, periodicJobId, 8000, 1000)
+        assertTrue(timelineAfterRestart*.status.any { it in ["RUNNING", 
"PENDING", "WAITING"] },
+                "periodic job should continue after FE restart, 
timeline=${timelineAfterRestart}")
+
+        def beforeMetrics = getPeriodicWarmupMetrics()
+        for (int i = 12; i < 16; i++) {
+            sql """INSERT INTO ${tableName} VALUES (${i}, 
'after_restart_${i}')"""
+        }
+        for (int i = 0; i < 1000; i++) {
+            sql """SELECT * FROM ${tableName}"""
+        }
+        def afterMetrics = waitForPeriodicWarmupFinish(beforeMetrics, 120000)
+        assertTrue(afterMetrics.finished > beforeMetrics.finished,
+                "periodic job should trigger another round after restart, 
before=${beforeMetrics}, after=${afterMetrics}")
+
+        sql """CANCEL WARM UP JOB WHERE ID = ${periodicJobId}"""
+        def cancelledRow = WarmupMetricsUtils.showWarmupJob(sqlRunner, 
periodicJobId)
+        assertEquals("CANCELLED", cancelledRow.status)
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_mixed_cluster_change_and_failover.groovy
 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_mixed_cluster_change_and_failover.groovy
new file mode 100644
index 00000000000..cf509bb7701
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_mixed_cluster_change_and_failover.groovy
@@ -0,0 +1,117 @@
+// 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.JsonSlurper
+import org.apache.doris.regression.suite.ClusterOptions
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_warm_up_mixed_cluster_change_and_failover', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+        'cloud_warm_up_table_filter_refresh_interval_ms=1000',
+    ]
+    options.beConfigs += [
+        'file_cache_enter_disk_resource_limit_mode_percent=99',
+        'enable_evict_file_cache_in_advance=false',
+        'file_cache_background_monitor_interval_ms=1000',
+    ]
+    options.cloudMode = true
+    options.beNum = 1
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+        def srcCluster = "warmup_change_src"
+        def dstCluster = "warmup_change_dst"
+        def unrelatedDst = "warmup_change_unrelated_dst"
+        def renamedDst = "warmup_change_dst_renamed"
+        def dbName = "test_warmup_mixed_cluster_change_db"
+        def tableName = "change_tbl"
+        def jobIds = []
+        def metaService = cluster.getAllMetaservices().get(0)
+        def jsonSlurper = new JsonSlurper()
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+        cluster.addBackend(1, unrelatedDst)
+
+        def getClusterId = { String clusterName ->
+            def tag = getCloudBeTagByName(clusterName)
+            jsonSlurper.parseText(tag).compute_group_id.toString()
+        }
+
+        sql """use @${srcCluster}"""
+        sql """CREATE DATABASE IF NOT EXISTS ${dbName}"""
+        sql """use ${dbName}"""
+        sql """CREATE TABLE IF NOT EXISTS ${tableName} (
+                   id INT,
+                   value STRING
+               )
+               DUPLICATE KEY(id)
+               DISTRIBUTED BY HASH(id) BUCKETS 1
+               PROPERTIES ("file_cache_ttl_seconds" = "3600")"""
+
+        def periodicJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            PROPERTIES ("sync_mode" = "periodic", "sync_interval_sec" = "1")
+        """)[0][0]
+        def eventJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            ON TABLES (INCLUDE '${dbName}.${tableName}')
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+        def unrelatedJobId = sql("""
+            WARM UP CLUSTER ${unrelatedDst} WITH CLUSTER ${srcCluster}
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+        jobIds.addAll([periodicJobId, eventJobId, unrelatedJobId])
+        WarmupMetricsUtils.waitForJobStatus(sqlRunner, periodicJobId, 
["RUNNING", "PENDING", "WAITING"], 60000)
+        WarmupMetricsUtils.waitForJobStatus(sqlRunner, eventJobId, ["RUNNING", 
"PENDING"], 60000)
+        WarmupMetricsUtils.waitForJobStatus(sqlRunner, unrelatedJobId, 
["RUNNING", "PENDING"], 60000)
+
+        def affectedBefore = 
WarmupMetricsUtils.collectWarmupJobsByCluster(sqlRunner, dstCluster)*.jobId
+        assertTrue(affectedBefore.contains(periodicJobId.toString()))
+        assertTrue(affectedBefore.contains(eventJobId.toString()))
+
+        sql """ALTER SYSTEM RENAME COMPUTE GROUP ${dstCluster} ${renamedDst}"""
+        WarmupMetricsUtils.assertAffectedJobsCancelled(sqlRunner,
+                [periodicJobId.toString(), eventJobId.toString()], 120000)
+        WarmupMetricsUtils.assertUnrelatedJobsUnaffected(sqlRunner, 
[unrelatedJobId.toString()])
+
+        sql """ALTER SYSTEM RENAME COMPUTE GROUP ${renamedDst} ${dstCluster}"""
+        def recreatedEventId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            ON TABLES (INCLUDE '${dbName}.${tableName}')
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+        jobIds << recreatedEventId
+
+        def unrelatedClusterId = getClusterId(unrelatedDst)
+        drop_cluster(unrelatedDst, unrelatedClusterId, metaService)
+        WarmupMetricsUtils.assertAffectedJobsCancelled(sqlRunner, 
[unrelatedJobId.toString()], 120000)
+        WarmupMetricsUtils.assertUnrelatedJobsUnaffected(sqlRunner, 
[recreatedEventId.toString()], ["RUNNING", "PENDING", "WAITING"])
+
+        jobIds.each { jid ->
+            try {
+                sql """CANCEL WARM UP JOB WHERE ID = ${jid}"""
+            } catch (Throwable ignored) {
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_normal_queue_semantics.groovy
 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_normal_queue_semantics.groovy
new file mode 100644
index 00000000000..0c6da79be07
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_normal_queue_semantics.groovy
@@ -0,0 +1,146 @@
+// 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 org.apache.doris.regression.suite.ClusterOptions
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_warm_up_normal_queue_semantics', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.beConfigs += [
+        'file_cache_enter_disk_resource_limit_mode_percent=99',
+        'enable_evict_file_cache_in_advance=false',
+        'file_cache_background_monitor_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def srcCluster = "warmup_queue_src"
+        def dstCluster = "warmup_queue_dst"
+        def auxCluster = "warmup_queue_aux"
+        def dbName = "test_warmup_queue_semantics_db"
+        def tableName = "queue_tbl"
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+        cluster.addBackend(1, auxCluster)
+
+        def restartMasterFe = {
+            def oldMasterFe = cluster.getMasterFe()
+            cluster.restartFrontends(oldMasterFe.index)
+            boolean restarted = false
+            for (int i = 0; i < 30; i++) {
+                if (cluster.getFeByIndex(oldMasterFe.index).alive) {
+                    restarted = true
+                    break
+                }
+                sleep(1000)
+            }
+            assertTrue(restarted)
+            context.reconnectFe()
+        }
+
+        sql """use @${srcCluster}"""
+        sql """CREATE DATABASE IF NOT EXISTS ${dbName}"""
+        sql """use ${dbName}"""
+        sql """DROP TABLE IF EXISTS ${tableName}"""
+        sql """CREATE TABLE ${tableName} (
+                   id INT,
+                   name STRING
+               )
+               DUPLICATE KEY(id)
+               DISTRIBUTED BY HASH(id) BUCKETS 1
+               PROPERTIES ("file_cache_ttl_seconds" = "3600")"""
+        for (int i = 0; i < 20; i++) {
+            sql """INSERT INTO ${tableName} VALUES (${i}, 'name_${i}')"""
+        }
+
+        def periodicJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            PROPERTIES (
+                "sync_mode" = "periodic",
+                "sync_interval_sec" = "1"
+            )
+        """)[0][0]
+        def onceTableJobId = sql("""WARM UP CLUSTER ${dstCluster} WITH TABLE 
${tableName}""")[0][0]
+        def onceClusterJobId = sql("""WARM UP CLUSTER ${dstCluster} WITH 
CLUSTER ${auxCluster}""")[0][0]
+        def eventJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            ON TABLES (INCLUDE '${dbName}.${tableName}')
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+
+        def queueRows = WarmupMetricsUtils.showWarmupJobsByDst(sqlRunner, 
dstCluster)
+        assert queueRows.find { it.jobId == periodicJobId.toString() } != null
+        assert queueRows.find { it.jobId == onceTableJobId.toString() } != null
+        assert queueRows.find { it.jobId == onceClusterJobId.toString() } != 
null
+        assert queueRows.find { it.jobId == eventJobId.toString() } != null
+
+        def normalJobIds = [periodicJobId, onceTableJobId, 
onceClusterJobId].collect { it.toString() }
+        WarmupMetricsUtils.waitForOnlyOneRunningNormalWarmup(sqlRunner, 
dstCluster, 30000, normalJobIds)
+        long runningNormal = 
WarmupMetricsUtils.countRunningNormalWarmupByDst(sqlRunner, dstCluster)
+        assertTrue(runningNormal <= 1,
+                "same dst cluster should have at most one running normal 
warmup, got ${runningNormal}")
+
+        def eventRow = WarmupMetricsUtils.showWarmupJob(sqlRunner, eventJobId)
+        assertTrue(WarmupMetricsUtils.isEventDrivenWarmupJob(eventRow))
+        assertTrue(eventRow.status in ["RUNNING", "PENDING", "WAITING"])
+
+        Map<String, Map> beforeRestartSnapshot = 
WarmupMetricsUtils.snapshotWarmupJobsById(sqlRunner)
+        restartMasterFe()
+        sql """use @${srcCluster}"""
+        sql """use ${dbName}"""
+        Map<String, Map> afterRestartSnapshot = 
WarmupMetricsUtils.waitForWarmupJobsRecovered(
+                sqlRunner, beforeRestartSnapshot, { before, current ->
+            [periodicJobId, onceTableJobId, onceClusterJobId, 
eventJobId].every { current.containsKey(it.toString()) }
+        }, 60000)
+        assertTrue(afterRestartSnapshot.keySet().containsAll(
+                [periodicJobId, onceTableJobId, onceClusterJobId, 
eventJobId].collect { it.toString() }))
+
+        WarmupMetricsUtils.waitForOnlyOneRunningNormalWarmup(sqlRunner, 
dstCluster, 30000, normalJobIds)
+        runningNormal = 
WarmupMetricsUtils.countRunningNormalWarmupByDst(sqlRunner, dstCluster)
+        assertTrue(runningNormal <= 1,
+                "after FE restart same dst cluster should still have at most 
one running normal warmup, "
+                        + "got ${runningNormal}")
+
+        def beforeMetrics = WarmupMetricsUtils.getWarmupMetrics(sqlRunner, 
srcCluster, dstCluster)
+        for (int i = 20; i < 24; i++) {
+            sql """INSERT INTO ${tableName} VALUES (${i}, 
'after_restart_${i}')"""
+        }
+        def afterMetrics = WarmupMetricsUtils.waitForWarmupFinish(sqlRunner, 
srcCluster, dstCluster,
+                beforeMetrics.finished + 1, 120000)
+        assertTrue(afterMetrics.finished >= beforeMetrics.finished + 1,
+                "event-driven warmup should finish after FE restart, 
before=${beforeMetrics}, after=${afterMetrics}")
+        assertTrue(afterMetrics.finished + afterMetrics.failed >= 
afterMetrics.submitted,
+                "event-driven warmup should drain submitted segments, 
after=${afterMetrics}")
+        assertTrue(afterMetrics.requested > beforeMetrics.requested,
+                "event-driven warmup should continue after FE restart, 
before=${beforeMetrics}, after=${afterMetrics}")
+
+        [onceTableJobId, onceClusterJobId, periodicJobId, eventJobId].each { 
jid ->
+            try {
+                sql """CANCEL WARM UP JOB WHERE ID = ${jid}"""
+            } catch (Throwable ignored) {
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/on_tables/test_warm_up_event_on_tables_overlap_semantics.groovy
 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/on_tables/test_warm_up_event_on_tables_overlap_semantics.groovy
new file mode 100644
index 00000000000..a7c8ef1bb2a
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/on_tables/test_warm_up_event_on_tables_overlap_semantics.groovy
@@ -0,0 +1,125 @@
+// 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 org.apache.doris.regression.suite.ClusterOptions
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_warm_up_event_on_tables_overlap_semantics', 'docker') {
+    def options = new ClusterOptions()
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'cloud_warm_up_job_scheduler_interval_millisecond=100',
+        'cloud_warm_up_table_filter_refresh_interval_ms=1000',
+    ]
+    options.beConfigs += [
+        'file_cache_enter_disk_resource_limit_mode_percent=99',
+        'enable_evict_file_cache_in_advance=false',
+        'file_cache_background_monitor_interval_ms=1000',
+    ]
+    options.cloudMode = true
+    options.beNum = 1
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def srcCluster = "warmup_overlap_src"
+        def dstCluster = "warmup_overlap_dst"
+        def dbName = "test_warmup_overlap_semantics_db"
+        def tableName = "overlap_tbl"
+        def otherTable = "overlap_other_tbl"
+        def jobIds = []
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+
+        sql """use @${srcCluster}"""
+        sql """CREATE DATABASE IF NOT EXISTS ${dbName}"""
+        sql """use ${dbName}"""
+        sql """CREATE TABLE IF NOT EXISTS ${tableName} (id INT, val STRING)
+               DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+               PROPERTIES ("file_cache_ttl_seconds" = "3600")"""
+        sql """CREATE TABLE IF NOT EXISTS ${otherTable} (id INT, val STRING)
+               DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+               PROPERTIES ("file_cache_ttl_seconds" = "3600")"""
+
+        def preciseJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            ON TABLES (INCLUDE '${dbName}.${tableName}')
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+        def overlapJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            ON TABLES (
+                INCLUDE '${dbName}.*',
+                EXCLUDE '${dbName}.${otherTable}'
+            )
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+        def containerJobId = sql("""
+            WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+            ON TABLES (INCLUDE '${dbName}.*')
+            PROPERTIES ("sync_mode" = "event_driven", "sync_event" = "load")
+        """)[0][0]
+        jobIds.addAll([preciseJobId, overlapJobId, containerJobId])
+
+        assert WarmupMetricsUtils.waitForMatchedTables(sqlRunner, preciseJobId,
+                ["${dbName}.${tableName}".toString()] as Set,
+                ["${dbName}.${otherTable}".toString()] as Set) ==
+                ["${dbName}.${tableName}".toString()] as Set
+        assert WarmupMetricsUtils.waitForMatchedTables(sqlRunner, overlapJobId,
+                ["${dbName}.${tableName}".toString()] as Set,
+                ["${dbName}.${otherTable}".toString()] as Set)
+                .contains("${dbName}.${tableName}".toString())
+        assert WarmupMetricsUtils.waitForMatchedTables(sqlRunner, 
containerJobId,
+                ["${dbName}.${tableName}".toString(), 
"${dbName}.${otherTable}".toString()] as Set)
+                .containsAll(["${dbName}.${tableName}".toString(), 
"${dbName}.${otherTable}".toString()] as Set)
+        jobIds.each { jobId ->
+            WarmupMetricsUtils.waitForJobStatus(sqlRunner, jobId, ["RUNNING"], 
60000)
+        }
+        Thread.sleep(1000)
+
+        def baseMetrics = WarmupMetricsUtils.getWarmupMetrics(sqlRunner, 
srcCluster, dstCluster)
+        for (int i = 0; i < 4; i++) {
+            sql """INSERT INTO ${tableName} VALUES (${i}, 'target_${i}')"""
+            sql """INSERT INTO ${otherTable} VALUES (${i}, 'other_${i}')"""
+        }
+        def finalMetrics = WarmupMetricsUtils.waitForWarmupFinish(sqlRunner, 
srcCluster, dstCluster,
+                baseMetrics.finished + 1, 120000)
+        assertTrue(finalMetrics.requested > baseMetrics.requested,
+                "overlap jobs should all receive triggers, 
before=${baseMetrics}, after=${finalMetrics}")
+
+        def preciseStats = WarmupMetricsUtils.waitForJobSyncStats(sqlRunner, 
preciseJobId, {
+            it.seg_num.requested_5m >= 4
+        }, 120000)
+        def overlapStats = WarmupMetricsUtils.waitForJobSyncStats(sqlRunner, 
overlapJobId, {
+            it.seg_num.requested_5m >= 4
+        }, 120000)
+        def containerStats = WarmupMetricsUtils.waitForJobSyncStats(sqlRunner, 
containerJobId, {
+            it.seg_num.requested_5m >= 8
+        }, 120000)
+        assertTrue(preciseStats.seg_num.requested_5m >= 4)
+        assertTrue(overlapStats.seg_num.requested_5m >= 4)
+        assertTrue(containerStats.seg_num.requested_5m >= 8)
+
+        jobIds.each { jid ->
+            try {
+                sql """CANCEL WARM UP JOB WHERE ID = ${jid}"""
+            } catch (Throwable ignored) {
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_allows_manual_periodic_jobs.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_allows_manual_periodic_jobs.groovy
new file mode 100644
index 00000000000..03f2f898c79
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_allows_manual_periodic_jobs.groovy
@@ -0,0 +1,103 @@
+// 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
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_allows_manual_periodic_jobs', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def srcCluster = "vcg_periodic_src"
+        def dstCluster = "vcg_periodic_dst"
+        def extraDst = "vcg_periodic_extra"
+        def vcgName = "vcgWarmupPeriodicAllowed"
+        def vcgId = "vcgWarmupPeriodicAllowedId"
+        def createdJobIds = []
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+        cluster.addBackend(1, extraDst)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def vcgBody = JsonOutput.toJson([
+                instance_id: instanceId,
+                cluster: [
+                        cluster_name   : vcgName,
+                        cluster_id     : vcgId,
+                        type           : "VIRTUAL",
+                        cluster_names  : [srcCluster, dstCluster],
+                        cluster_policy : [type: "ActiveStandby", 
active_cluster_name: srcCluster,
+                                          standby_cluster_names: [dstCluster]]
+                ]
+        ])
+
+        try {
+            addClusterApi(vcgBody) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            def autoRows = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, srcCluster, dstCluster, 
2, 120000)
+            createdJobIds.addAll(autoRows*.jobId)
+            assertTrue(autoRows.any { it.syncMode.startsWith("PERIODIC") })
+            assertTrue(autoRows.any { it.syncMode.startsWith("EVENT_DRIVEN") })
+
+            def manualPeriodicId = sql("""
+                WARM UP CLUSTER ${extraDst} WITH CLUSTER ${srcCluster}
+                PROPERTIES ("sync_mode" = "periodic", "sync_interval_sec" = 
"600")
+            """)[0][0]
+            createdJobIds << manualPeriodicId.toString()
+
+            def manualPeriodicRow = 
WarmupMetricsUtils.showWarmupJob(sqlRunner, manualPeriodicId)
+            assertEquals(srcCluster, manualPeriodicRow.srcCluster)
+            assertEquals(extraDst, manualPeriodicRow.dstCluster)
+            assertTrue(manualPeriodicRow.syncMode.startsWith("PERIODIC"))
+
+            def originalAutoRows = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, srcCluster, dstCluster, 
2, 30000)
+            
assertTrue(originalAutoRows*.jobId.toSet().containsAll(autoRows*.jobId as Set))
+        } finally {
+            createdJobIds.each { jobId ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_failover_cancels_old_jobs.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_failover_cancels_old_jobs.groovy
new file mode 100644
index 00000000000..e2bb6a68876
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_failover_cancels_old_jobs.groovy
@@ -0,0 +1,125 @@
+// 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
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_failover_cancels_old_jobs', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def clusterA = "vcg_failover_a"
+        def clusterB = "vcg_failover_b"
+        def vcgName = "vcgWarmupFailoverCancel"
+        def vcgId = "vcgWarmupFailoverCancelId"
+
+        cluster.addBackend(2, clusterA)
+        cluster.addBackend(2, clusterB)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def buildBody = { String active, String standby ->
+            JsonOutput.toJson([
+                    instance_id: instanceId,
+                    cluster: [
+                            cluster_name   : vcgName,
+                            cluster_id     : vcgId,
+                            type           : "VIRTUAL",
+                            cluster_names  : [clusterA, clusterB],
+                            cluster_policy : [type: "ActiveStandby", 
active_cluster_name: active,
+                                              standby_cluster_names: 
[standby], failover_failure_threshold: 3]
+                    ]
+            ])
+        }
+
+        try {
+            addClusterApi(buildBody(clusterA, clusterB)) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            def oldRows = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, clusterA, clusterB, 2, 
120000)
+            def oldJobIds = oldRows*.jobId
+            assertTrue(oldRows.any { it.syncMode.startsWith("PERIODIC") })
+            assertTrue(oldRows.any { it.syncMode.startsWith("EVENT_DRIVEN") })
+
+            sql """USE @${vcgName}"""
+            sql """DROP TABLE IF EXISTS test_vcg_warmup_failover_cancel_tbl"""
+            sql """
+                CREATE TABLE test_vcg_warmup_failover_cancel_tbl (
+                    k1 INT
+                )
+                DUPLICATE KEY(k1)
+                DISTRIBUTED BY HASH(k1) BUCKETS 1
+                PROPERTIES (
+                    "replication_num" = "1"
+                )
+            """
+            sql """INSERT INTO test_vcg_warmup_failover_cancel_tbl VALUES 
(1)"""
+
+            cluster.stopBackends(4, 5)
+            awaitUntil(50, 3) {
+                sql """USE @${vcgName}"""
+                sql """SELECT COUNT(*) FROM 
test_vcg_warmup_failover_cancel_tbl"""
+                def groups = sql_return_maparray """SHOW COMPUTE GROUPS"""
+                def vcg = groups.find { it.Name == vcgName }
+                vcg != null && 
vcg.Policy.contains("\"activeComputeGroup\":\"${clusterB}\"")
+            }
+
+            WarmupMetricsUtils.assertHistoricalJobsCancelled(sqlRunner, 
oldJobIds, 120000)
+            def newJobIds = 
WarmupMetricsUtils.waitForVcgWarmupRecreated(sqlRunner, clusterB, clusterA,
+                    oldJobIds, 2, 120000)
+            assertTrue(newJobIds.size() >= 2)
+            def newRows = newJobIds.collect { 
WarmupMetricsUtils.showWarmupJob(sqlRunner, it) }
+            assertTrue(newRows.any { it.syncMode.startsWith("PERIODIC") })
+            assertTrue(newRows.any { it.syncMode.startsWith("EVENT_DRIVEN") })
+        } finally {
+            cluster.startBackends(4, 5)
+            WarmupMetricsUtils.showWarmupJobs(sqlRunner).findAll {
+                it.jobId != null && (
+                        (it.srcCluster == clusterA && it.dstCluster == 
clusterB)
+                                || (it.srcCluster == clusterB && it.dstCluster 
== clusterA)
+                )
+            }.each { row ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${row.jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_precedence_and_reconcile.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_precedence_and_reconcile.groovy
new file mode 100644
index 00000000000..9e416ec104d
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_precedence_and_reconcile.groovy
@@ -0,0 +1,153 @@
+// 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
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_precedence_and_reconcile', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+        'cloud_warm_up_table_filter_refresh_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def srcCluster = "vcg_prec_src"
+        def dstCluster = "vcg_prec_dst"
+        def extraCluster = "vcg_prec_extra"
+        def vcgName = "vcgWarmupPrecedence"
+        def vcgId = "vcgWarmupPrecedenceId"
+        def dbName = "test_vcg_precedence_db"
+        def tableName = "vcg_tbl"
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+        cluster.addBackend(1, extraCluster)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def vcgBody = {
+            def clusterPolicy = [type: "ActiveStandby", active_cluster_name: 
srcCluster, standby_cluster_names: [dstCluster]]
+            def clusterMap = [cluster_name: vcgName, cluster_id: vcgId, type: 
"VIRTUAL",
+                              cluster_names: [srcCluster, dstCluster], 
cluster_policy: clusterPolicy]
+            return JsonOutput.toJson([instance_id: instanceId, cluster: 
clusterMap])
+        }
+
+        def knownJobIds = []
+        try {
+            sql """use @${srcCluster}"""
+            sql """CREATE DATABASE IF NOT EXISTS ${dbName}"""
+            sql """use ${dbName}"""
+            sql """CREATE TABLE IF NOT EXISTS ${tableName} (
+                       id INT,
+                       value STRING
+                   )
+                   DUPLICATE KEY(id)
+                   DISTRIBUTED BY HASH(id) BUCKETS 1
+                   PROPERTIES ("file_cache_ttl_seconds" = "3600")"""
+
+            def tableFirstJobId = sql("""
+                WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+                ON TABLES (INCLUDE '${dbName}.${tableName}')
+                PROPERTIES ("sync_mode" = "event_driven", "sync_event" = 
"load")
+            """)[0][0]
+            knownJobIds << tableFirstJobId.toString()
+
+            addClusterApi(vcgBody()) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            WarmupMetricsUtils.assertHistoricalJobsCancelled(sqlRunner, 
[tableFirstJobId.toString()], 120000)
+            def cancelledTableJob = 
WarmupMetricsUtils.showWarmupJob(sqlRunner, tableFirstJobId)
+            assertTrue(cancelledTableJob.errMsg.contains(
+                    "vcg cancel table-level load-event warm up job before 
rebuilding file cache jobs"))
+            def newAutoJobs = 
WarmupMetricsUtils.waitForVcgWarmupRecreated(sqlRunner, srcCluster, dstCluster,
+                    [tableFirstJobId.toString()], 2, 120000)
+            assertTrue(newAutoJobs.size() >= 2,
+                    "vcg should recreate auto warmup jobs after table-level 
cancellation, newJobs=${newAutoJobs}")
+            knownJobIds.addAll(newAutoJobs)
+            def autoRows = newAutoJobs.collect { 
WarmupMetricsUtils.showWarmupJob(sqlRunner, it) }
+            assertTrue(autoRows.any { it.syncMode.startsWith("EVENT_DRIVEN") 
&& it.type == "CLUSTER" })
+            assertTrue(autoRows.any { it.syncMode.startsWith("PERIODIC") && 
it.type == "CLUSTER" })
+
+            String clusterConflict = 
WarmupMetricsUtils.expectCreateConflict(sqlRunner, {
+                sql("""
+                    WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+                    PROPERTIES ("sync_mode" = "event_driven", "sync_event" = 
"load")
+                """)
+            })
+            WarmupMetricsUtils.assertConflictMessage(clusterConflict, ["Cannot 
create warm up job", srcCluster, dstCluster])
+
+            String tableConflict = 
WarmupMetricsUtils.expectCreateConflict(sqlRunner, {
+                sql("""
+                    WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+                    ON TABLES (INCLUDE '${dbName}.${tableName}')
+                    PROPERTIES ("sync_mode" = "event_driven", "sync_event" = 
"load")
+                """)
+            })
+            WarmupMetricsUtils.assertConflictMessage(tableConflict, ["Cannot 
create warm up job", srcCluster, dstCluster])
+
+            def secondBody = JsonOutput.toJson([
+                    instance_id: instanceId,
+                    cluster: [
+                            cluster_name   : "vcgWarmupPrecedenceShared",
+                            cluster_id     : "vcgWarmupPrecedenceSharedId",
+                            type           : "VIRTUAL",
+                            cluster_names  : [srcCluster, extraCluster],
+                            cluster_policy : [type: "ActiveStandby", 
active_cluster_name: srcCluster,
+                                              standby_cluster_names: 
[extraCluster]]
+                    ]
+            ])
+            addClusterApi(secondBody) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(!json.code.equalsIgnoreCase("OK"))
+            }
+        } finally {
+            knownJobIds.each { jobId ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+            try {
+                sql """use @${srcCluster}"""
+                sql """use ${dbName}"""
+                sql """DROP TABLE IF EXISTS ${tableName}"""
+                sql """DROP DATABASE IF EXISTS ${dbName}"""
+            } catch (Throwable ignored) {
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_restart_master_fe.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_restart_master_fe.groovy
new file mode 100644
index 00000000000..a301e53ea27
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_restart_master_fe.groovy
@@ -0,0 +1,140 @@
+// 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 groovy.json.JsonSlurper
+import org.apache.doris.regression.suite.ClusterOptions
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_restart_master_fe', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def srcCluster = "vcg_restart_src"
+        def dstCluster = "vcg_restart_dst"
+        def vcgName = "vcgWarmupRestartMasterFe"
+        def vcgId = "vcgWarmupRestartMasterFeId"
+        def knownJobIds = []
+        def jsonSlurper = new JsonSlurper()
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def restartMasterFe = {
+            def oldMasterFe = cluster.getMasterFe()
+            cluster.restartFrontends(oldMasterFe.index)
+            boolean restarted = false
+            for (int i = 0; i < 30; i++) {
+                if (cluster.getFeByIndex(oldMasterFe.index).alive) {
+                    restarted = true
+                    break
+                }
+                sleep(1000)
+            }
+            assertTrue(restarted)
+            context.reconnectFe()
+        }
+
+        def getVcgWarmupJobIds = {
+            def groups = sql_return_maparray """SHOW COMPUTE GROUPS"""
+            def vcg = groups.find { it.Name == vcgName }
+            if (vcg == null) {
+                return []
+            }
+            def policy = jsonSlurper.parseText(vcg.Policy)
+            def rawJobIds = policy.cacheWarmupJobIds ?: "[]"
+            if (rawJobIds instanceof Collection) {
+                return rawJobIds.collect { it.toString() }
+            }
+            return jsonSlurper.parseText(rawJobIds.toString()).collect { 
it.toString() }
+        }
+
+        def waitForVcgWarmupJobIds = { Collection<String> excludedJobIds ->
+            List<String> jobIds = []
+            awaitUntil(120, 1) {
+                jobIds = getVcgWarmupJobIds().findAll {
+                    !excludedJobIds.contains(it)
+                }
+                jobIds.size() >= 2
+            }
+            return jobIds
+        }
+
+        def vcgBody = JsonOutput.toJson([
+                instance_id: instanceId,
+                cluster: [
+                        cluster_name   : vcgName,
+                        cluster_id     : vcgId,
+                        type           : "VIRTUAL",
+                        cluster_names  : [srcCluster, dstCluster],
+                        cluster_policy : [type: "ActiveStandby", 
active_cluster_name: srcCluster,
+                                          standby_cluster_names: [dstCluster]]
+                ]
+        ])
+
+        try {
+            addClusterApi(vcgBody) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            def beforeRestartRows = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, srcCluster, dstCluster, 
2, 120000)
+            knownJobIds.addAll(beforeRestartRows*.jobId)
+            assertTrue(beforeRestartRows.any { 
it.syncMode.startsWith("PERIODIC") })
+            assertTrue(beforeRestartRows.any { 
it.syncMode.startsWith("EVENT_DRIVEN") })
+            assertTrue(getVcgWarmupJobIds().containsAll(knownJobIds))
+
+            restartMasterFe()
+
+            WarmupMetricsUtils.assertHistoricalJobsCancelled(sqlRunner, 
knownJobIds, 120000)
+            def newJobIds = waitForVcgWarmupJobIds(knownJobIds)
+            assertTrue(newJobIds.size() >= 2)
+            def rowsAfterRestart = newJobIds.collect { 
WarmupMetricsUtils.showWarmupJob(sqlRunner, it) }
+            assertTrue(rowsAfterRestart.any { 
it.syncMode.startsWith("PERIODIC") && it.status in ["RUNNING", "PENDING", 
"WAITING"] })
+            assertTrue(rowsAfterRestart.any { 
it.syncMode.startsWith("EVENT_DRIVEN") && it.status in ["RUNNING", "PENDING"] })
+            knownJobIds.addAll(newJobIds)
+        } finally {
+            knownJobIds.each { jobId ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_restart_target_be.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_restart_target_be.groovy
new file mode 100644
index 00000000000..0f798b84d95
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_restart_target_be.groovy
@@ -0,0 +1,130 @@
+// 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
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_restart_target_be', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.beConfigs += [
+        'file_cache_enter_disk_resource_limit_mode_percent=99',
+        'enable_evict_file_cache_in_advance=false',
+        'file_cache_background_monitor_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def srcCluster = "vcg_be_restart_src"
+        def dstCluster = "vcg_be_restart_dst"
+        def vcgName = "vcgWarmupRestartBe"
+        def vcgId = "vcgWarmupRestartBeId"
+        def dbName = "test_vcg_warmup_restart_be_db"
+        def tableName = "be_restart_tbl"
+        def knownJobIds = []
+
+        cluster.addBackend(1, srcCluster)
+        def dstBeIndexes = cluster.addBackend(1, dstCluster)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def vcgBody = JsonOutput.toJson([
+                instance_id: instanceId,
+                cluster: [
+                        cluster_name   : vcgName,
+                        cluster_id     : vcgId,
+                        type           : "VIRTUAL",
+                        cluster_names  : [srcCluster, dstCluster],
+                        cluster_policy : [type: "ActiveStandby", 
active_cluster_name: srcCluster,
+                                          standby_cluster_names: [dstCluster]]
+                ]
+        ])
+
+        try {
+            sql """use @${srcCluster}"""
+            sql """CREATE DATABASE IF NOT EXISTS ${dbName}"""
+            sql """use ${dbName}"""
+            sql """CREATE TABLE IF NOT EXISTS ${tableName} (
+                       id INT,
+                       value STRING
+                   )
+                   DUPLICATE KEY(id)
+                   DISTRIBUTED BY HASH(id) BUCKETS 1
+                   PROPERTIES ("file_cache_ttl_seconds" = "3600")"""
+
+            addClusterApi(vcgBody) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            def beforeRestartRows = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, srcCluster, dstCluster, 
2, 120000)
+            knownJobIds.addAll(beforeRestartRows*.jobId)
+            assertTrue(beforeRestartRows.any { 
it.syncMode.startsWith("PERIODIC") })
+            assertTrue(beforeRestartRows.any { 
it.syncMode.startsWith("EVENT_DRIVEN") })
+
+            cluster.restartBackends(dstBeIndexes[0] as int)
+            sleep(8000)
+            sql """use @${srcCluster}"""
+            sql """use ${dbName}"""
+
+            def afterRestartRows = knownJobIds.collect { 
WarmupMetricsUtils.showWarmupJob(sqlRunner, it) }
+            assertTrue(afterRestartRows.any { 
it.syncMode.startsWith("PERIODIC") && it.status in ["RUNNING", "PENDING", 
"WAITING"] })
+            assertTrue(afterRestartRows.any { 
it.syncMode.startsWith("EVENT_DRIVEN") && it.status in ["RUNNING", "PENDING"] })
+
+            def beforeMetrics = WarmupMetricsUtils.getWarmupMetrics(sqlRunner, 
srcCluster, dstCluster)
+            for (int i = 0; i < 4; i++) {
+                sql """INSERT INTO ${tableName} VALUES (${i}, 
'after_be_restart_${i}')"""
+            }
+            def afterMetrics = 
WarmupMetricsUtils.waitForWarmupFinish(sqlRunner, srcCluster, dstCluster,
+                    beforeMetrics.finished + 1, 120000)
+            assertTrue(afterMetrics.requested > beforeMetrics.requested,
+                    "vcg auto event-driven warmup should continue after target 
BE restart, before=${beforeMetrics}, after=${afterMetrics}")
+        } finally {
+            knownJobIds.each { jobId ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+            try {
+                sql """use @${srcCluster}"""
+                sql """use ${dbName}"""
+                sql """DROP TABLE IF EXISTS ${tableName}"""
+                sql """DROP DATABASE IF EXISTS ${dbName}"""
+            } catch (Throwable ignored) {
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_shared_compute_group_conflict.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_shared_compute_group_conflict.groovy
new file mode 100644
index 00000000000..71cffefba8f
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_shared_compute_group_conflict.groovy
@@ -0,0 +1,99 @@
+// 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
+
+suite('test_vcg_warmup_shared_compute_group_conflict', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def clusterA = "vcg_share_a"
+        def clusterB = "vcg_share_b"
+        def clusterC = "vcg_share_c"
+
+        cluster.addBackend(1, clusterA)
+        cluster.addBackend(1, clusterB)
+        cluster.addBackend(1, clusterC)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def firstBody = JsonOutput.toJson([
+                instance_id: instanceId,
+                cluster: [
+                        cluster_name   : "vcgShareFirst",
+                        cluster_id     : "vcgShareFirstId",
+                        type           : "VIRTUAL",
+                        cluster_names  : [clusterA, clusterB],
+                        cluster_policy : [type: "ActiveStandby", 
active_cluster_name: clusterA,
+                                          standby_cluster_names: [clusterB]]
+                ]
+        ])
+        addClusterApi(firstBody) { respCode, body ->
+            def json = parseJson(body)
+            assertTrue(json.code.equalsIgnoreCase("OK"))
+        }
+
+        def secondBody = JsonOutput.toJson([
+                instance_id: instanceId,
+                cluster: [
+                        cluster_name   : "vcgShareSecond",
+                        cluster_id     : "vcgShareSecondId",
+                        type           : "VIRTUAL",
+                        cluster_names  : [clusterA, clusterC],
+                        cluster_policy : [type: "ActiveStandby", 
active_cluster_name: clusterA,
+                                          standby_cluster_names: [clusterC]]
+                ]
+        ])
+        addClusterApi(secondBody) { respCode, body ->
+            def json = parseJson(body)
+            assertTrue(!json.code.equalsIgnoreCase("OK"))
+        }
+
+        def reverseSharedBody = JsonOutput.toJson([
+                instance_id: instanceId,
+                cluster: [
+                        cluster_name   : "vcgShareThird",
+                        cluster_id     : "vcgShareThirdId",
+                        type           : "VIRTUAL",
+                        cluster_names  : [clusterC, clusterB],
+                        cluster_policy : [type: "ActiveStandby", 
active_cluster_name: clusterC,
+                                          standby_cluster_names: [clusterB]]
+                ]
+        ])
+        addClusterApi(reverseSharedBody) { respCode, body ->
+            def json = parseJson(body)
+            assertTrue(!json.code.equalsIgnoreCase("OK"))
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_switch_and_drop_cancels_history.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_switch_and_drop_cancels_history.groovy
new file mode 100644
index 00000000000..d6410518fab
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_switch_and_drop_cancels_history.groovy
@@ -0,0 +1,130 @@
+// 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
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_switch_and_drop_cancels_history', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def srcCluster = "vcg_switch_src"
+        def dstCluster = "vcg_switch_dst"
+        def vcgName = "vcgWarmupSwitchHistory"
+        def vcgId = "vcgWarmupSwitchHistoryId"
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+
+        def logShowClusters = { String message ->
+            def rows = sql """SHOW CLUSTERS"""
+            log.info("{}: {}", message, rows)
+        }
+
+        def logWarmupJobs = { String message, Collection jobIds ->
+            jobIds.each { jobId ->
+                def rows = sql """SHOW WARM UP JOB WHERE ID = ${jobId}"""
+                log.info("{} jobId={}: {}", message, jobId, rows)
+            }
+        }
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def alterClusterInfoApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/alter_vcluster_info?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def buildVcgBody = { String active, String standby ->
+            def clusterPolicy = [type: "ActiveStandby", active_cluster_name: 
active,
+                                 standby_cluster_names: [standby]]
+            def clusterMap = [cluster_name: vcgName, cluster_id: vcgId, type: 
"VIRTUAL",
+                              cluster_names: [srcCluster, dstCluster], 
cluster_policy: clusterPolicy]
+            return JsonOutput.toJson([instance_id: instanceId, cluster: 
clusterMap])
+        }
+
+        try {
+            addClusterApi(buildVcgBody(srcCluster, dstCluster)) { respCode, 
body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+            logShowClusters("show clusters after vcg create")
+
+            def initialRows = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, srcCluster, dstCluster, 
2, 120000)
+            def initialJobIds = initialRows*.jobId
+            log.info("show warm up jobs after vcg create: {}", initialRows)
+            logWarmupJobs("show warm up job detail after vcg create", 
initialJobIds)
+            assertTrue(initialRows.any { it.type == "CLUSTER" && 
it.syncMode.startsWith("PERIODIC") })
+            assertTrue(initialRows.any { it.type == "CLUSTER" && 
it.syncMode.startsWith("EVENT_DRIVEN") })
+
+            alterClusterInfoApi(buildVcgBody(dstCluster, srcCluster)) { 
respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            WarmupMetricsUtils.assertHistoricalJobsCancelled(sqlRunner, 
initialJobIds, 120000)
+            def switchedJobIds = 
WarmupMetricsUtils.waitForVcgWarmupRecreated(sqlRunner, dstCluster, srcCluster,
+                    initialJobIds, 2, 120000)
+            assertTrue(switchedJobIds.size() >= 2)
+            def switchedRows = switchedJobIds.collect { 
WarmupMetricsUtils.showWarmupJob(sqlRunner, it) }
+            log.info("show warm up jobs after vcg switch: {}", switchedRows)
+            logWarmupJobs("show warm up job detail after vcg switch", 
switchedJobIds)
+            assertTrue(switchedRows.any { it.type == "CLUSTER" && 
it.syncMode.startsWith("PERIODIC") })
+            assertTrue(switchedRows.any { it.type == "CLUSTER" && 
it.syncMode.startsWith("EVENT_DRIVEN") })
+
+            drop_cluster(vcgName, vcgId, ms)
+            WarmupMetricsUtils.assertHistoricalJobsCancelled(sqlRunner, 
switchedJobIds, 120000)
+            logWarmupJobs("show warm up job detail after vcg drop", 
switchedJobIds)
+        } finally {
+            WarmupMetricsUtils.showWarmupJobs(sqlRunner).findAll {
+                it.jobId != null && (
+                        (it.srcCluster == srcCluster && it.dstCluster == 
dstCluster)
+                                || (it.srcCluster == dstCluster && 
it.dstCluster == srcCluster)
+                )
+            }.each { row ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${row.jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_waits_for_existing_cluster_jobs.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_waits_for_existing_cluster_jobs.groovy
new file mode 100644
index 00000000000..b9faa241b57
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_waits_for_existing_cluster_jobs.groovy
@@ -0,0 +1,107 @@
+// 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
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_waits_for_existing_cluster_jobs', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def srcCluster = "vcg_wait_src"
+        def dstCluster = "vcg_wait_dst"
+        def vcgName = "vcgWarmupWaitsExisting"
+        def vcgId = "vcgWarmupWaitsExistingId"
+        def allKnownJobIds = []
+
+        cluster.addBackend(1, srcCluster)
+        cluster.addBackend(1, dstCluster)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def vcgBody = {
+            def clusterPolicy = [type: "ActiveStandby", active_cluster_name: 
srcCluster,
+                                 standby_cluster_names: [dstCluster]]
+            def clusterMap = [cluster_name: vcgName, cluster_id: vcgId, type: 
"VIRTUAL",
+                              cluster_names: [srcCluster, dstCluster], 
cluster_policy: clusterPolicy]
+            return JsonOutput.toJson([instance_id: instanceId, cluster: 
clusterMap])
+        }
+
+        try {
+            def periodicJobId = sql("""
+                WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+                PROPERTIES ("sync_mode" = "periodic", "sync_interval_sec" = 
"600")
+            """)[0][0]
+            def clusterEventJobId = sql("""
+                WARM UP CLUSTER ${dstCluster} WITH CLUSTER ${srcCluster}
+                PROPERTIES ("sync_mode" = "event_driven", "sync_event" = 
"load")
+            """)[0][0]
+            allKnownJobIds.addAll([periodicJobId.toString(), 
clusterEventJobId.toString()])
+
+            addClusterApi(vcgBody()) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            sleep(8000)
+            def rowsBeforeCancel = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, srcCluster, dstCluster, 
2, 30000)
+            assertEquals(2, rowsBeforeCancel.size())
+            assertTrue(rowsBeforeCancel*.jobId.toSet() == 
[periodicJobId.toString(), clusterEventJobId.toString()] as Set,
+                    "vcg should wait existing cluster-level jobs before 
creating auto jobs, rows=${rowsBeforeCancel}")
+
+            sql """CANCEL WARM UP JOB WHERE ID = ${periodicJobId}"""
+            sql """CANCEL WARM UP JOB WHERE ID = ${clusterEventJobId}"""
+            WarmupMetricsUtils.assertHistoricalJobsCancelled(sqlRunner,
+                    [periodicJobId.toString(), clusterEventJobId.toString()], 
120000)
+
+            def recreatedJobIds = 
WarmupMetricsUtils.waitForVcgWarmupRecreated(sqlRunner, srcCluster, dstCluster,
+                    [periodicJobId.toString(), clusterEventJobId.toString()], 
2, 120000)
+            allKnownJobIds.addAll(recreatedJobIds)
+            assertTrue(recreatedJobIds.size() >= 2)
+            def recreatedRows = recreatedJobIds.collect { 
WarmupMetricsUtils.showWarmupJob(sqlRunner, it) }
+            assertTrue(recreatedRows.any { it.type == "CLUSTER" && 
it.syncMode.startsWith("PERIODIC") })
+            assertTrue(recreatedRows.any { it.type == "CLUSTER" && 
it.syncMode.startsWith("EVENT_DRIVEN") })
+        } finally {
+            allKnownJobIds.each { jobId ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_with_manual_once_queue_semantics.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_with_manual_once_queue_semantics.groovy
new file mode 100644
index 00000000000..7932d9d3681
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_with_manual_once_queue_semantics.groovy
@@ -0,0 +1,111 @@
+// 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
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_with_manual_once_queue_semantics', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def vcgSrc = "vcg_queue_src"
+        def sharedDst = "vcg_queue_dst"
+        def manualSrc = "vcg_queue_manual_src"
+        def vcgName = "vcgWarmupManualOnceQueue"
+        def vcgId = "vcgWarmupManualOnceQueueId"
+        def createdJobIds = []
+
+        cluster.addBackend(1, vcgSrc)
+        cluster.addBackend(1, sharedDst)
+        cluster.addBackend(1, manualSrc)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def vcgBody = JsonOutput.toJson([
+                instance_id: instanceId,
+                cluster: [
+                        cluster_name   : vcgName,
+                        cluster_id     : vcgId,
+                        type           : "VIRTUAL",
+                        cluster_names  : [vcgSrc, sharedDst],
+                        cluster_policy : [type: "ActiveStandby", 
active_cluster_name: vcgSrc,
+                                          standby_cluster_names: [sharedDst]]
+                ]
+        ])
+
+        try {
+            addClusterApi(vcgBody) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            def autoRows = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, vcgSrc, sharedDst, 2, 
120000)
+            createdJobIds.addAll(autoRows*.jobId)
+            assertTrue(autoRows.any { it.syncMode.startsWith("PERIODIC") })
+            assertTrue(autoRows.any { it.syncMode.startsWith("EVENT_DRIVEN") })
+
+            def manualOnceId = sql("""WARM UP CLUSTER ${sharedDst} WITH 
CLUSTER ${manualSrc}""")[0][0]
+            createdJobIds << manualOnceId.toString()
+
+            def rowsByDst = WarmupMetricsUtils.showWarmupJobsByDst(sqlRunner, 
sharedDst)
+            assertTrue(rowsByDst*.jobId.contains(manualOnceId.toString()))
+
+            def expectedNormalJobIds = (autoRows.findAll { 
WarmupMetricsUtils.isNormalWarmupJob(it) }*.jobId
+                    + [manualOnceId.toString()])
+            WarmupMetricsUtils.waitForOnlyOneRunningNormalWarmup(sqlRunner, 
sharedDst, 30000, expectedNormalJobIds)
+            long runningNormal = 
WarmupMetricsUtils.countRunningNormalWarmupByDst(sqlRunner, sharedDst)
+            assertTrue(runningNormal <= 1,
+                    "vcg auto periodic plus manual once should still serialize 
normal warmup on same dst, "
+                            + "running=${runningNormal}")
+
+            def manualOnceRow = WarmupMetricsUtils.showWarmupJob(sqlRunner, 
manualOnceId)
+            assertTrue(WarmupMetricsUtils.isNormalWarmupJob(manualOnceRow))
+            assertEquals(sharedDst, manualOnceRow.dstCluster)
+
+            def eventRows = rowsByDst.findAll { 
it.syncMode.startsWith("EVENT_DRIVEN") }
+            assertTrue(eventRows.size() >= 1)
+            assertTrue(eventRows.every { it.status in ["RUNNING", "PENDING"] })
+        } finally {
+            createdJobIds.each { jobId ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_with_manual_periodic_queue_semantics.groovy
 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_with_manual_periodic_queue_semantics.groovy
new file mode 100644
index 00000000000..185013a9e1b
--- /dev/null
+++ 
b/regression-test/suites/cloud_p0/multi_cluster/virtual_compute_group/test_vcg_warmup_with_manual_periodic_queue_semantics.groovy
@@ -0,0 +1,110 @@
+// 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
+import org.apache.doris.regression.util.WarmupMetricsUtils
+
+suite('test_vcg_warmup_with_manual_periodic_queue_semantics', 'docker') {
+    def options = new ClusterOptions()
+    options.feNum = 3
+    options.feConfigs += [
+        'cloud_cluster_check_interval_second=1',
+        'sys_log_verbose_modules=org',
+        'fetch_cluster_cache_hotspot_interval_ms=1000',
+    ]
+    options.cloudMode = true
+
+    docker(options) {
+        Closure sqlRunner = { String q -> sql(q) }
+
+        def ms = cluster.getAllMetaservices().get(0)
+        def msHttpPort = ms.host + ":" + ms.httpPort
+        def instanceId = "default_instance_id"
+        def vcgSrc = "vcg_periodic_queue_src"
+        def sharedDst = "vcg_periodic_queue_dst"
+        def manualSrc = "vcg_periodic_queue_manual_src"
+        def vcgName = "vcgWarmupManualPeriodicQueue"
+        def vcgId = "vcgWarmupManualPeriodicQueueId"
+        def createdJobIds = []
+
+        cluster.addBackend(1, vcgSrc)
+        cluster.addBackend(1, sharedDst)
+        cluster.addBackend(1, manualSrc)
+
+        def addClusterApi = { requestBody, Closure checkFunc ->
+            httpTest {
+                endpoint msHttpPort
+                uri "/MetaService/http/add_cluster?token=$token"
+                body requestBody
+                check checkFunc
+            }
+        }
+
+        def vcgBody = JsonOutput.toJson([
+                instance_id: instanceId,
+                cluster: [
+                        cluster_name   : vcgName,
+                        cluster_id     : vcgId,
+                        type           : "VIRTUAL",
+                        cluster_names  : [vcgSrc, sharedDst],
+                        cluster_policy : [type: "ActiveStandby", 
active_cluster_name: vcgSrc,
+                                          standby_cluster_names: [sharedDst]]
+                ]
+        ])
+
+        try {
+            addClusterApi(vcgBody) { respCode, body ->
+                def json = parseJson(body)
+                assertTrue(json.code.equalsIgnoreCase("OK"))
+            }
+
+            def autoRows = 
WarmupMetricsUtils.waitForWarmupJobsByPair(sqlRunner, vcgSrc, sharedDst, 2, 
120000)
+            createdJobIds.addAll(autoRows*.jobId)
+            assertTrue(autoRows.any { it.syncMode.startsWith("PERIODIC") })
+            assertTrue(autoRows.any { it.syncMode.startsWith("EVENT_DRIVEN") })
+
+            def manualPeriodicId = sql("""
+                WARM UP CLUSTER ${sharedDst} WITH CLUSTER ${manualSrc}
+                PROPERTIES ("sync_mode" = "periodic", "sync_interval_sec" = 
"600")
+            """)[0][0]
+            createdJobIds << manualPeriodicId.toString()
+
+            def rowsByDst = WarmupMetricsUtils.showWarmupJobsByDst(sqlRunner, 
sharedDst)
+            assertTrue(rowsByDst*.jobId.contains(manualPeriodicId.toString()))
+
+            def expectedNormalJobIds = (autoRows.findAll { 
WarmupMetricsUtils.isNormalWarmupJob(it) }*.jobId
+                    + [manualPeriodicId.toString()])
+            WarmupMetricsUtils.waitForOnlyOneRunningNormalWarmup(sqlRunner, 
sharedDst, 30000, expectedNormalJobIds)
+            long runningNormal = 
WarmupMetricsUtils.countRunningNormalWarmupByDst(sqlRunner, sharedDst)
+            assertTrue(runningNormal <= 1,
+                    "vcg auto periodic plus manual periodic should still 
serialize normal warmup on same dst, "
+                            + "running=${runningNormal}")
+
+            def manualPeriodicRow = 
WarmupMetricsUtils.showWarmupJob(sqlRunner, manualPeriodicId)
+            assertTrue(manualPeriodicRow.syncMode.startsWith("PERIODIC"))
+            assertEquals(sharedDst, manualPeriodicRow.dstCluster)
+        } finally {
+            createdJobIds.each { jobId ->
+                try {
+                    sql """CANCEL WARM UP JOB WHERE ID = ${jobId}"""
+                } catch (Throwable ignored) {
+                }
+            }
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to