github-actions[bot] commented on code in PR #65153:
URL: https://github.com/apache/doris/pull/65153#discussion_r3568137704


##########
fe/fe-core/src/main/java/org/apache/doris/cloud/CloudWarmUpJob.java:
##########
@@ -1123,6 +1128,13 @@ private void runRunningJob() throws Exception {
                         if (this.isPeriodic()) {
                             // wait for next schedule
                             this.jobState = JobState.PENDING;
+                            long nextScheduleTimeMs = finishedTimeMs + 
syncInterval * 1000;

Review Comment:
   This log does not match the scheduler condition it is trying to explain. 
`shouldWait()` delays periodic jobs until `startTimeMs + syncInterval`, but 
this diagnostic reports `finishedTimeMs + syncInterval` and computes 
`triggerImmediately` from that later time. For any run that lasts longer than 
the interval, the scheduler can run again immediately while the log says the 
next schedule is still in the future. Please compute the logged next 
schedule/trigger flag from the same timestamp basis as `shouldWait()` (or 
change the scheduler if the intended semantics are delay-after-finish).



##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy:
##########
@@ -265,4 +265,259 @@ 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 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) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        long lastCount = -1
+        while (System.currentTimeMillis() < deadline) {
+            lastCount = countRunningNormalWarmupByDst(sqlRunner, dstCluster)
+            if (lastCount <= 1) {

Review Comment:
   This can return before the scheduler has exercised the lock path. Right 
after the tests create the normal warm-up jobs they can all still be `PENDING`, 
because `tryRegisterRunningJob()` is only called from `runPendingJob()` when 
the scheduler runs; that zero-running snapshot satisfies `lastCount <= 1`. The 
new queue tests can therefore pass without proving that one normal job is 
running while another same-destination normal job is kept out, especially after 
FE restart. Please wait until at least one expected normal job has actually 
reached `RUNNING` or attempted to run, then assert over a scheduler cycle that 
no second normal job becomes `RUNNING`.



##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy:
##########
@@ -265,4 +265,259 @@ 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 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) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        long lastCount = -1
+        while (System.currentTimeMillis() < deadline) {
+            lastCount = countRunningNormalWarmupByDst(sqlRunner, dstCluster)
+            if (lastCount <= 1) {
+                return
+            }
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException("expected at most one running normal warmup 
on ${dstCluster}, last=${lastCount}")
+    }
+
+    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)
+        }
+        return [states: states, row: showWarmupJob(sqlRunner, jobId)]

Review Comment:
   This helper can return a timeout result that the new scheduler test still 
treats as success. The loop only observes the requested cycle when `transitions 
>= minTransitions`, but after the deadline it returns the states anyway; a job 
that goes `[PENDING, RUNNING]` and never reschedules before timeout satisfies 
the caller's `contains("RUNNING")` and `contains(PENDING/WAITING)` assertions 
without ever proving a RUNNING-to-PENDING/WAITING periodic cycle. Please make 
the helper fail on timeout or return/assert the transition count so the test 
only passes after the requested cycle was actually observed.



##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/util/WarmupMetricsUtils.groovy:
##########
@@ -265,4 +265,259 @@ 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 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) {
+        long deadline = System.currentTimeMillis() + timeoutMs
+        long lastCount = -1
+        while (System.currentTimeMillis() < deadline) {
+            lastCount = countRunningNormalWarmupByDst(sqlRunner, dstCluster)
+            if (lastCount <= 1) {
+                return
+            }
+            Thread.sleep(1000)
+        }
+        throw new RuntimeException("expected at most one running normal warmup 
on ${dstCluster}, last=${lastCount}")
+    }
+
+    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)
+        }
+        return [states: states, row: showWarmupJob(sqlRunner, jobId)]
+    }
+
+    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<String> getVcgWarmupJobIds(Closure sqlRunner, String 
srcCluster, String dstCluster) {
+        return showWarmupJobs(sqlRunner).findAll {
+            it.srcCluster == srcCluster && it.dstCluster == dstCluster

Review Comment:
   These helpers should not treat every source/destination match as a recreated 
active VCG warm-up job. `SHOW WARM UP JOB` is backed by `cloudWarmUpJobs`; 
cancelled/finished jobs are removed from `runnableCloudWarmUpJobs` but stay 
visible until the history TTL. Because this filter ignores status, type, and 
sync mode, `waitForVcgWarmupRecreated()` can return historical non-old jobs and 
the VCG tests can pass without active `CLUSTER` periodic plus event-driven jobs 
being rebuilt. Please filter or validate active statuses and the expected 
`CLUSTER` periodic/event-driven modes before treating recreation as successful.



##########
regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_normal_queue_semantics.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 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
+
+        WarmupMetricsUtils.waitForOnlyOneRunningNormalWarmup(sqlRunner, 
dstCluster, 30000)
+        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)
+        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.requested > beforeMetrics.requested,

Review Comment:
   This can still pass when the destination never finishes the warm-up. 
`waitForWarmupFinish()` only logs and returns a snapshot on timeout, and 
`requested` comes from the source cluster, so a load event can increment 
`requested` while the target side never reaches `finished >= before.finished + 
1`. The same pattern appears in the new overlap and VCG target-restart tests. 
Please either make the helper fail on timeout or assert the returned metrics 
actually satisfy the destination completion condition you waited for, for 
example `afterMetrics.finished >= beforeMetrics.finished + 1` and 
`afterMetrics.finished + afterMetrics.failed >= afterMetrics.submitted`.



##########
docker/runtime/doris-compose/utils.py:
##########
@@ -251,17 +269,17 @@ def _command_available(cmd):
             )
         return cmd
 
-    # Prefer Compose v2.
-    if _command_available(["docker", "compose", "version"]):
+    # Prefer Compose v2 when it can talk to the daemon.
+    if _compose_v2_usable(["docker", "compose"]):
         return ["docker", "compose"]
 
-    # Fallback for old machines.
+    # Fallback for old machines or environments with a newer standalone client.
     if _command_available(["docker-compose", "version"]):

Review Comment:
   Since the v2 path only accepts `docker compose` after `compose ls` talks to 
the daemon, this fallback should apply an equivalent daemon-touching check 
before returning `docker-compose`. In a mixed install, `docker compose ls` can 
fail for the API/daemon mismatch this PR is trying to catch, but 
`docker-compose version` can still succeed, so detection returns the standalone 
client and the real `up`/`stop` command fails later. Please either verify the 
standalone client with a daemon command too, or narrow the function 
contract/error text so it does not claim the returned client is usable with the 
current daemon.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to