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 5cf524587c6 branch-4.1: [fix](regression) Stop MTMV task waits from
latching onto the previous task #67710 (#67821)
5cf524587c6 is described below
commit 5cf524587c6dfd132ab995533546e8c189e601e8
Author: yujun <[email protected]>
AuthorDate: Sat Sep 12 00:55:26 2026 +0800
branch-4.1: [fix](regression) Stop MTMV task waits from latching onto the
previous task #67710 (#67821)
cherry-pick: #67710
---
.../org/apache/doris/regression/suite/Suite.groovy | 224 +++++++++++++--------
.../regression/suite/SuiteMTMVTaskWaitTest.groovy | 76 +++++++
2 files changed, 214 insertions(+), 86 deletions(-)
diff --git
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
index cbae49d7b10..d894e961e85 100644
---
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
+++
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
@@ -113,6 +113,10 @@ class Suite implements GroovyInterceptable {
private AmazonS3 s3Client = null
private FileSystem fs = null
+ // MTMV task ids that a wait in this suite already returned as terminal. A
task that just
+ // finished is briefly absent from tasks(), so a later wait can fall back
to one of these.
+ private final Set<String> finishedMTMVTaskIds =
Collections.synchronizedSet(new HashSet<>())
+
Suite(String name, String group, SuiteContext context, SuiteCluster
cluster) {
this.name = name
this.group = group
@@ -2003,73 +2007,135 @@ class Suite implements GroovyInterceptable {
return debugPoint
}
- def waitingMTMVTaskFinishedByMvName = { mvName, dbName = context.dbName ->
- Thread.sleep(2000);
- String showTasks = "select
TaskId,JobId,JobName,MvId,Status,MvName,MvDatabaseName,ErrorMsg from
tasks('type'='mv') where MvDatabaseName = '${dbName}' and MvName = '${mvName}'
order by CreateTime ASC"
+ /**
+ * Poll a tasks('type'='mv') query until the newest row is terminal and
return that row.
+ * A task that just finished is briefly missing from tasks(): the job
removes it from its
+ * running list before the MV history gets it, so while that window is
open the newest row
+ * is the previous task of the same MV. A terminal row is therefore
trusted only when the
+ * same task was already seen running by this wait, or is seen terminal
twice in a row and
+ * no earlier wait returned it; the window lasts tens of milliseconds, far
less than the
+ * poll interval, so one extra poll is enough to tell the new task from
the previous one
+ * even when the previous task was not waited for before.
+ * poll() runs the query once, log() receives the progress messages, and
the two intervals
+ * exist so tests can shorten them. The last row seen is returned when the
timeout expires,
+ * so callers can report a status.
+ */
+ static List<Object> pollMTMVTaskTerminal(String showTasks, String caller,
Set<String> finishedTaskIds,
+ Closure<List<List<Object>>> poll, Closure<String> log, long
pollIntervalMs, long skipFinishedMs) {
String status = "NULL"
- List<List<Object>> result
- long startTime = System.currentTimeMillis()
- long timeoutTimestamp = startTime + 5 * 60 * 1000 // 5 min
- List<String> toCheckTaskRow = new ArrayList<>();
- while (timeoutTimestamp > System.currentTimeMillis() && (status ==
'PENDING' || status == 'RUNNING' || status == 'NULL')) {
- result = sql(showTasks)
- logger.info("current db is " + dbName + ", showTasks is " +
result.toString())
+ String runningTaskId = null
+ String confirmedTaskId = null
+ String lastLoggedStatus = null
+ List<Object> taskRow = null
+ long skipFinishedDeadline = 0
+ long timeoutTimestamp = System.currentTimeMillis() + 5 * 60 * 1000 //
5 min
+ while (timeoutTimestamp > System.currentTimeMillis()) {
+ List<List<Object>> result = poll()
if (result.isEmpty()) {
- logger.info("waitingMTMVTaskFinishedByMvName toCheckTaskRow is
empty")
- Thread.sleep(1000);
+ if (lastLoggedStatus != "NULL") {
+ log("${caller} task row is empty")
+ lastLoggedStatus = "NULL"
+ }
+ confirmedTaskId = null
+ Thread.sleep(pollIntervalMs);
continue;
}
- toCheckTaskRow = result.last();
- status = toCheckTaskRow.get(4)
- logger.info("The state of ${showTasks} is ${status}")
- Thread.sleep(1000);
+ taskRow = result[0]
+ String taskId = taskRow.get(0).toString()
+ status = taskRow.get(1).toString()
+ if (lastLoggedStatus != status) {
+ log("The state of ${showTasks} is ${status}, taskId is
${taskId}")
+ lastLoggedStatus = status
+ }
+ if (status == 'PENDING' || status == 'RUNNING') {
+ runningTaskId = taskId
+ confirmedTaskId = null
+ Thread.sleep(pollIntervalMs);
+ continue;
+ }
+ if (finishedTaskIds.contains(taskId)) {
+ // A wait already returned this task, so it is the previous
task and the one
+ // being waited for is still in the gap. Skip it for as long
as any real gap
+ // could last; a caller that waits twice for the same task
(nothing new was
+ // submitted) then falls back to it instead of hitting the 5
minute timeout.
+ if (skipFinishedDeadline == 0) {
+ skipFinishedDeadline = System.currentTimeMillis() +
skipFinishedMs
+ }
+ if (System.currentTimeMillis() < skipFinishedDeadline) {
+ confirmedTaskId = null
+ Thread.sleep(pollIntervalMs);
+ continue;
+ }
+ }
+ if (taskId == runningTaskId || taskId == confirmedTaskId) {
+ finishedTaskIds.add(taskId)
+ return taskRow
+ }
+ // The first sighting of a terminal task may be the previous task
seen through the
+ // gap described above; confirm the same task once more before
trusting it.
+ confirmedTaskId = taskId
+ Thread.sleep(pollIntervalMs);
}
+ return taskRow
+ }
+
+ /**
+ * Wait for the newest MTMV task matching showTasks to reach a terminal
state and return
+ * its row. See pollMTMVTaskTerminal for why a terminal row needs to be
confirmed.
+ */
+ List<Object> waitMTMVTaskTerminal(String showTasks, String caller,
+ long pollIntervalMs = 500, long skipFinishedMs = 30 * 1000) {
+ return pollMTMVTaskTerminal(showTasks, caller, finishedMTMVTaskIds,
+ { -> sql(showTasks) }, { String message ->
logger.info(message) },
+ pollIntervalMs, skipFinishedMs)
+ }
+
+ def waitingMTMVTaskFinishedByMvName = { mvName, dbName = context.dbName ->
+ // Wait for the newly submitted MTMV task to become visible in tasks().
+ Thread.sleep(2000);
+ String showTasks = """
+ select TaskId, Status, MvName, MvDatabaseName from
tasks('type'='mv')
+ where MvDatabaseName = '${dbName}' and MvName = '${mvName}'
+ order by CreateTime DESC limit 1
+ """
+ List<Object> toCheckTaskRow = waitMTMVTaskTerminal(showTasks,
"waitingMTMVTaskFinishedByMvName")
+ String status = toCheckTaskRow == null ? "NULL" :
toCheckTaskRow.get(1).toString()
if (status != "SUCCESS") {
- logger.info("status is not success")
+ logger.info("status is ${status}")
}
Assert.assertEquals("SUCCESS", status)
def show_tables = sql """
- show tables from ${toCheckTaskRow.get(6)};
+ show tables from ${toCheckTaskRow.get(3)};
"""
- def db_id = getDbId(toCheckTaskRow.get(6))
- def table_id = getTableId(toCheckTaskRow.get(6), mvName)
+ def db_id = getDbId(toCheckTaskRow.get(3))
+ def table_id = getTableId(toCheckTaskRow.get(3), mvName)
logger.info("waitingMTMVTaskFinished analyze mv name is " + mvName
- + ", db name is " + toCheckTaskRow.get(6)
+ + ", db name is " + toCheckTaskRow.get(3)
+ ", show_tables are " + show_tables
+ ", db_id is " + db_id
+ ", table_id " + table_id)
- sql "analyze table ${toCheckTaskRow.get(6)}.${mvName} with sync;"
+ sql "analyze table ${toCheckTaskRow.get(3)}.${mvName} with sync;"
}
def waitingMTMVTaskFinishedByMvNameAllowCancel = {mvName, dbName =
context.dbName ->
+ // Wait for the newly submitted MTMV task to become visible in tasks().
Thread.sleep(2000);
- String showTasks = "select
TaskId,JobId,JobName,MvId,Status,MvName,MvDatabaseName,ErrorMsg from
tasks('type'='mv') where MvDatabaseName = '${dbName}' and MvName = '${mvName}'
order by CreateTime ASC"
-
- String status = "NULL"
- List<List<Object>> result
- long startTime = System.currentTimeMillis()
- long timeoutTimestamp = startTime + 5 * 60 * 1000 // 5 min
- List<String> toCheckTaskRow = new ArrayList<>();
- while (timeoutTimestamp > System.currentTimeMillis() && (status ==
'PENDING' || status == 'RUNNING' || status == 'NULL' || status == 'CANCELED'))
{
- result = sql(showTasks)
- logger.info("current db is " + dbName + ", showTasks result: " +
result.toString())
- if (result.isEmpty()) {
- logger.info("waitingMTMVTaskFinishedByMvName toCheckTaskRow is
empty")
- Thread.sleep(1000);
- continue;
- }
- toCheckTaskRow = result.last()
- status = toCheckTaskRow.get(4)
- logger.info("The state of ${showTasks} is ${status}")
- Thread.sleep(1000);
- }
+ String showTasks = """
+ select TaskId, Status, MvName, MvDatabaseName, ErrorMsg from
tasks('type'='mv')
+ where MvDatabaseName = '${dbName}' and MvName = '${mvName}'
+ order by CreateTime DESC limit 1
+ """
+
+ List<Object> toCheckTaskRow = waitMTMVTaskTerminal(showTasks,
"waitingMTMVTaskFinishedByMvNameAllowCancel")
+ String status = toCheckTaskRow == null ? "NULL" :
toCheckTaskRow.get(1).toString()
if (status != "SUCCESS") {
logger.info("status is not success")
- assertTrue(result.toString().contains("same table"))
+ Assert.assertNotNull(toCheckTaskRow)
+ assertTrue(String.valueOf(toCheckTaskRow.get(4)).contains("same
table"))
}
// Need to analyze materialized view for cbo to choose the
materialized view accurately
- logger.info("waitingMTMVTaskFinished analyze mv name is " +
toCheckTaskRow.get(5))
- sql "analyze table ${toCheckTaskRow.get(6)}.${mvName} with sync;"
+ logger.info("waitingMTMVTaskFinished analyze mv name is " +
toCheckTaskRow.get(2))
+ sql "analyze table ${toCheckTaskRow.get(3)}.${mvName} with sync;"
}
void waitingMVTaskFinishedByMvName(String dbName, String tableName, String
indexName) {
@@ -2134,61 +2200,47 @@ class Suite implements GroovyInterceptable {
}
void waitingMTMVTaskFinished(String jobName) {
+ // Wait for the newly submitted MTMV task to become visible in tasks().
Thread.sleep(2000);
- String showTasks = "select
TaskId,JobId,JobName,MvId,Status,MvName,MvDatabaseName,ErrorMsg from
tasks('type'='mv') where JobName = '${jobName}' order by CreateTime ASC"
- String status = "NULL"
- List<List<Object>> result
- long startTime = System.currentTimeMillis()
- long timeoutTimestamp = startTime + 5 * 60 * 1000 // 5 min
- do {
- result = sql(showTasks)
- logger.info("result: " + result.toString())
- if (!result.isEmpty()) {
- status = result.last().get(4)
- }
- logger.info("The state of ${showTasks} is ${status}")
- Thread.sleep(1000);
- } while (timeoutTimestamp > System.currentTimeMillis() && (status ==
'PENDING' || status == 'RUNNING' || status == 'NULL'))
+ String showTasks = """
+ select TaskId, Status, MvName, MvDatabaseName from
tasks('type'='mv')
+ where JobName = '${jobName}' order by CreateTime DESC limit 1
+ """
+ List<Object> taskRow = waitMTMVTaskTerminal(showTasks,
"waitingMTMVTaskFinished")
+ String status = taskRow == null ? "NULL" : taskRow.get(1).toString()
if (status != "SUCCESS") {
- logger.info("status is not success")
+ logger.info("status is ${status}")
}
Assert.assertEquals("SUCCESS", status)
// Need to analyze materialized view for cbo to choose the
materialized view accurately
def show_tables = sql """
- show tables from ${result.last().get(6)};
+ show tables from ${taskRow.get(3)};
"""
- def db_id = getDbId(result.last().get(6))
- def table_id = getTableId(result.last().get(6), result.last().get(5))
- logger.info("waitingMTMVTaskFinished analyze mv name is " +
result.last().get(5)
- + ", db name is " + result.last().get(6)
+ def db_id = getDbId(taskRow.get(3))
+ def table_id = getTableId(taskRow.get(3), taskRow.get(2))
+ logger.info("waitingMTMVTaskFinished analyze mv name is " +
taskRow.get(2)
+ + ", db name is " + taskRow.get(3)
+ ", show_tables are " + show_tables
+ ", db_id is " + db_id
+ ", table_id " + table_id)
- sql "analyze table ${result.last().get(6)}.${result.last().get(5)}
with sync;"
- String db = result.last().get(6)
- String table = result.last().get(5)
- result = sql("show table stats ${db}.${table}")
- logger.info("table stats: " + result.toString())
- result = sql("show index stats ${db}.${table} ${table}")
- logger.info("index stats: " + result.toString())
+ sql "analyze table ${taskRow.get(3)}.${taskRow.get(2)} with sync;"
+ String db = taskRow.get(3)
+ String table = taskRow.get(2)
+ def stats = sql("show table stats ${db}.${table}")
+ logger.info("table stats: " + stats.toString())
+ stats = sql("show index stats ${db}.${table} ${table}")
+ logger.info("index stats: " + stats.toString())
}
void waitingMTMVTaskFinishedNotNeedSuccess(String jobName) {
+ // Wait for the newly submitted MTMV task to become visible in tasks().
Thread.sleep(2000);
- String showTasks = "select
TaskId,JobId,JobName,MvId,Status,MvName,MvDatabaseName,ErrorMsg from
tasks('type'='mv') where JobName = '${jobName}' order by CreateTime ASC"
- String status = "NULL"
- List<List<Object>> result
- long startTime = System.currentTimeMillis()
- long timeoutTimestamp = startTime + 5 * 60 * 1000 // 5 min
- do {
- result = sql(showTasks)
- logger.info("result: " + result.toString())
- if (!result.isEmpty()) {
- status = result.last().get(4)
- }
- logger.info("The state of ${showTasks} is ${status}")
- Thread.sleep(1000);
- } while (timeoutTimestamp > System.currentTimeMillis() && (status ==
'PENDING' || status == 'RUNNING' || status == 'NULL'))
+ String showTasks = """
+ select TaskId, Status from tasks('type'='mv')
+ where JobName = '${jobName}' order by CreateTime DESC limit 1
+ """
+ List<Object> taskRow = waitMTMVTaskTerminal(showTasks,
"waitingMTMVTaskFinishedNotNeedSuccess")
+ String status = taskRow == null ? "NULL" : taskRow.get(1).toString()
if (status != "SUCCESS") {
logger.info("status is not success")
}
diff --git
a/regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/SuiteMTMVTaskWaitTest.groovy
b/regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/SuiteMTMVTaskWaitTest.groovy
new file mode 100644
index 00000000000..39e5a7d8ea6
--- /dev/null
+++
b/regression-test/framework/src/test/groovy/org/apache/doris/regression/suite/SuiteMTMVTaskWaitTest.groovy
@@ -0,0 +1,76 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.regression.suite
+
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+
+class SuiteMTMVTaskWaitTest {
+ private static final String SHOW_TASKS = "select TaskId, Status from
tasks('type'='mv')"
+ private static final Closure<String> IGNORE_LOG = { String message -> }
+
+ private static List<Object> task(String taskId, String status) {
+ return [taskId, status] as List<Object>
+ }
+
+ private static Closure<List<List<Object>>> polls(List<List<List<Object>>>
script) {
+ Deque<List<List<Object>>> queue = new ArrayDeque<>(script)
+ return { ->
+ if (queue.isEmpty()) {
+ throw new IllegalStateException("poll script exhausted, the
wait did not converge")
+ }
+ return queue.poll()
+ }
+ }
+
+ private static String waitFor(Set<String> finished,
List<List<List<Object>>> script) {
+ return Suite.pollMTMVTaskTerminal(SHOW_TASKS, "test", finished,
polls(script), IGNORE_LOG, 1, 5)
+ .get(0).toString()
+ }
+
+ @Test
+ void followsATaskThatWasSeenRunning() {
+ assertEquals("X", waitFor(new HashSet<>(), [[task("X", "RUNNING")],
[task("X", "SUCCESS")]]))
+ }
+
+ @Test
+ void confirmsATerminalRowThatWasNeverSeenRunning() {
+ assertEquals("X", waitFor(new HashSet<>(), [[task("X", "SUCCESS")],
[task("X", "SUCCESS")]]))
+ }
+
+ @Test
+ void ignoresThePreviousTaskWhileTheNewTaskIsMissing() {
+ Set<String> finished = new HashSet<>()
+ assertEquals("P", waitFor(finished, [[task("P", "RUNNING")],
[task("P", "SUCCESS")]]))
+
+ // X runs and finishes; while it moves from the running list to the MV
history the
+ // newest row of the MV is P again, and P must not be returned a
second time.
+ assertEquals("X", waitFor(finished, [[task("X", "RUNNING")],
[task("P", "SUCCESS")],
+ [task("X", "SUCCESS")]]))
+ }
+
+ @Test
+ void fallsBackToTheSameTaskWhenNothingNewIsSubmitted() {
+ Set<String> finished = new HashSet<>()
+ assertEquals("P", waitFor(finished, [[task("P", "RUNNING")],
[task("P", "SUCCESS")]]))
+
+ // Nothing new was submitted: the wait must return P instead of
polling to the timeout.
+ assertEquals("P", waitFor(finished, (1..100).collect { [task("P",
"SUCCESS")] }))
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]