This is an automated email from the ASF dual-hosted git repository.
ruanwenjun pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git
The following commit(s) were added to refs/heads/dev by this push:
new 7cead6c895 [Fix-17854] [Worker&SQL Task] Fix SQL task query result
alert not being sent (#18549)
7cead6c895 is described below
commit 7cead6c895dbd0bc9e04ea2300c22c6bdff4fcb9
Author: suyc <[email protected]>
AuthorDate: Tue Oct 13 15:33:12 2026 +0800
[Fix-17854] [Worker&SQL Task] Fix SQL task query result alert not being
sent (#18549)
---
deploy/kubernetes/dolphinscheduler/README.md | 3 +
deploy/kubernetes/dolphinscheduler/values.yaml | 9 +
docs/docs/en/architecture/configuration.md | 3 +
docs/docs/en/guide/upgrade/incompatible.md | 5 +
docs/docs/en/guide/upgrade/upgrade.md | 22 ++
docs/docs/zh/architecture/configuration.md | 3 +
docs/docs/zh/guide/upgrade/incompatible.md | 5 +
docs/docs/zh/guide/upgrade/upgrade.md | 22 ++
.../alert/service/AlertSender.java | 7 +
.../alert/runner/AlertSenderTest.java | 30 ++
.../common/constants/Constants.java | 17 +
.../dolphinscheduler/common/enums/AlertType.java | 3 +-
.../src/main/resources/common.properties | 8 +
.../src/test/resources/common.properties | 8 +-
.../org/apache/dolphinscheduler/dao/AlertDao.java | 25 ++
.../apache/dolphinscheduler/dao/entity/Alert.java | 6 +
.../lifecycle/event/TaskSuccessLifecycleEvent.java | 6 +
.../handler/TaskSuccessLifecycleEventHandler.java | 24 +-
.../master/rpc/TaskExecutorEventListenerImpl.java | 2 +
.../TaskSuccessLifecycleEventHandlerTest.java | 173 +++++++++
.../service/alert/WorkflowAlertManager.java | 28 ++
.../service/alert/WorkflowAlertManagerTest.java | 123 +++++++
.../events/TaskExecutorSuccessLifecycleEvent.java | 7 +
.../plugin/task/api/AbstractTask.java | 51 ++-
.../plugin/task/api/TaskExecutionContext.java | 5 +
.../plugin/task/api/model/TaskAlertInfo.java | 5 +
.../plugin/task/api/AbstractTaskTest.java | 54 +++
.../dolphinscheduler/plugin/task/sql/SqlTask.java | 149 +++++++-
.../plugin/task/sql/SqlTaskTest.java | 393 +++++++++++++++++++++
.../worker/executor/PhysicalTaskExecutor.java | 8 +
30 files changed, 1184 insertions(+), 20 deletions(-)
diff --git a/deploy/kubernetes/dolphinscheduler/README.md
b/deploy/kubernetes/dolphinscheduler/README.md
index 69b5590a0e..99de38ec81 100644
--- a/deploy/kubernetes/dolphinscheduler/README.md
+++ b/deploy/kubernetes/dolphinscheduler/README.md
@@ -203,6 +203,9 @@ The paths above match the API and Worker server classpaths.
Check the configurat
| conf.common."resource.storage.type" | string | `"S3"` | resource storage
type: HDFS, S3, OSS, GCS, ABS, NONE |
| conf.common."resource.storage.upload.base.path" | string |
`"/dolphinscheduler"` | resource store on HDFS/S3 path, resource file will
store to this base path, self configuration, please make sure the directory
exists on hdfs and have read write permissions. "/dolphinscheduler" is
recommended |
| conf.common."shell.kill.wait.timeout" | int | `10` | If the shell process is
still active after this timeout value (in seconds), then will use kill -9 to
kill it |
+| conf.common."sql.task.alert.max.content.bytes" | int | `60000` | max
serialized UTF-8 bytes of the SQL task-result alert content, kept below the
65535-byte capacity of the MySQL TEXT column |
+| conf.common."sql.task.alert.max.row.value.length" | int | `100` | max chars
of a single string value kept in the SQL task-result alert content, longer
values are truncated with an ellipsis |
+| conf.common."sql.task.alert.max.rows" | int | `30` | max rows kept in the
SQL task-result alert content, rows beyond this are replaced by an omission
marker |
| conf.common."sudo.enable" | bool | `true` | use sudo or not, if set true,
executing user is tenant user and deploy user needs sudo permissions; if set
false, executing user is the deploy user and doesn't need sudo permissions |
| conf.common."support.hive.oneSession" | bool | `false` | Whether hive SQL is
executed in the same session |
| conf.common."task.resource.limit.state" | bool | `false` | Task resource
limit state |
diff --git a/deploy/kubernetes/dolphinscheduler/values.yaml
b/deploy/kubernetes/dolphinscheduler/values.yaml
index 048da5aa7f..8f168534e4 100644
--- a/deploy/kubernetes/dolphinscheduler/values.yaml
+++ b/deploy/kubernetes/dolphinscheduler/values.yaml
@@ -393,6 +393,15 @@ conf:
# -- way to collect applicationId: log, aop
appId.collect: log
+ # -- max rows kept in the SQL task-result alert content, rows beyond this
are replaced by an omission marker
+ sql.task.alert.max.rows: 30
+
+ # -- max chars of a single string value kept in the SQL task-result alert
content, longer values are truncated with an ellipsis
+ sql.task.alert.max.row.value.length: 100
+
+ # -- max serialized UTF-8 bytes of the SQL task-result alert content, kept
below the 65535-byte capacity of the MySQL TEXT column
+ sql.task.alert.max.content.bytes: 60000
+
common:
## Configmap
configmap:
diff --git a/docs/docs/en/architecture/configuration.md
b/docs/docs/en/architecture/configuration.md
index cb37799a85..5b5c128ba4 100644
--- a/docs/docs/en/architecture/configuration.md
+++ b/docs/docs/en/architecture/configuration.md
@@ -223,6 +223,9 @@ The default configuration is as follows:
| sudo.enable | true
| whether to enable sudo
[...]
| zeppelin.rest.url | http://localhost:8080
| the RESTful API url of zeppelin
[...]
| appId.collect | log
| way to collect applicationId, if use aop, alter the
configuration from log to aop, annotation of applicationId auto collection
related configuration in `bin/env/dolphinscheduler_env.sh` should be removed.
Note: Aop way doesn't support submitting yarn job on remote host by client mode
like Beeline, and will failure if override applicationId collection-related
environment configuration in d [...]
+| sql.task.alert.max.rows | 30
| max rows kept in the SQL task-result alert content, rows
beyond this are replaced by an omission marker. Takes effect after restarting
Worker Server
[...]
+| sql.task.alert.max.row.value.length | 100
| max chars of a single string value kept in the SQL
task-result alert content, longer values are truncated with an ellipsis. Takes
effect after restarting Worker Server
[...]
+| sql.task.alert.max.content.bytes | 60000
| max serialized UTF-8 bytes of the SQL task-result alert
content, kept below the 65535-byte capacity of the MySQL TEXT column that
stores alert content. Takes effect after restarting Worker Server
[...]
### Api-server related configuration
diff --git a/docs/docs/en/guide/upgrade/incompatible.md
b/docs/docs/en/guide/upgrade/incompatible.md
index 650b52df38..2b6649dad2 100644
--- a/docs/docs/en/guide/upgrade/incompatible.md
+++ b/docs/docs/en/guide/upgrade/incompatible.md
@@ -57,3 +57,8 @@ This document records the incompatible updates between each
version. You need to
* **Removed derived properties**: `cmdTypeIfComplement`, `complementData`
(related to complement-data executions; use the detail API to obtain them)
* To obtain any of these fields, use the detail API `GET
/projects/{projectCode}/workflow-instances/{id}` instead, which continues to
return the full `WorkflowInstance` object.
([#18444](https://github.com/apache/dolphinscheduler/pull/18444))
+## 3.5.0
+
+* `AlertSender#getAlertData` now explicitly rejects a null `alertType` by
throwing `IllegalArgumentException` with an error log, instead of throwing a
raw `NullPointerException`. During a rolling upgrade, **the Alert Server must
be upgraded before Master/Worker**, otherwise an old Alert Server cannot
recognize newly introduced `alert_type` enum values (e.g. `TASK_RESULT`) and
alert delivery will fail. See the [upgrade
guide](upgrade.md#rolling-upgrade-order) for details.([#18549](https:/ [...]
+* SQL task-result alert content is size-limited: by default at most 30 rows
are kept, string field values longer than 100 characters are truncated with an
ellipsis, and the serialized content is bounded to 60000 UTF-8 bytes (below the
65535-byte capacity of the MySQL TEXT column); rows beyond the budget are
represented by an omission marker. These defaults are tunable via
`sql.task.alert.max.rows`, `sql.task.alert.max.row.value.length` and
`sql.task.alert.max.content.bytes` in `common.pr [...]
+
diff --git a/docs/docs/en/guide/upgrade/upgrade.md
b/docs/docs/en/guide/upgrade/upgrade.md
index dfb3016a07..67b631e1fe 100644
--- a/docs/docs/en/guide/upgrade/upgrade.md
+++ b/docs/docs/en/guide/upgrade/upgrade.md
@@ -75,6 +75,28 @@ Execution result:
- After version 3.3.X and later, we only support upgrading from 3.0.0. For
versions lower than this, please download the historical version and upgrade to
3.0.0.
- After version 3.3.X and later, binary packages no longer provide plugins
dependencies by default, so when you use them for the first time, you need to
download and install them yourself. For more information, please refer to
[Pseudo-Cluster](../installation/pseudo-cluster.md).
+#### Rolling Upgrade Order
+
+When performing a rolling upgrade, **the Alert Server must be upgraded before
Master/Worker**.
+
+Starting from 3.5.0, the Master may persist new `alert_type` enum values (e.g.
`TASK_RESULT`) into the `t_ds_alert` table. If the Alert Server has not yet
been upgraded to a version that includes the new enum value, MyBatis will
deserialize the unknown value as `null`, causing `AlertSender` to throw a
`NullPointerException` while building the alert data. The alert will remain
stuck in `WAIT_EXECUTION` and never be delivered.
+
+Upgrading in the order "Alert Server → Master → Worker" eliminates this risk.
+
+#### If the upgrade fails
+
+If the upgrade order was not followed (e.g. Master/Worker has already been
upgraded before the Alert Server) and new `alert_type` values have been
persisted to the `t_ds_alert` table, follow these steps to recover:
+
+1. Upgrade the Alert Server to a version that includes the new enum values and
restart it. Alerts stuck in `WAIT_EXECUTION` will be picked up and delivered
normally.
+2. If the Alert Server cannot be upgraded immediately, stop the old Alert
Server first, then back up and clean up the affected alerts so they cannot keep
blocking alert delivery:
+
+ ```sql
+ -- 1. back up the affected alerts
+ create table t_ds_alert_bak as select * from t_ds_alert where alert_type =
8;
+ -- 2. remove them from the active table
+ delete from t_ds_alert where alert_type = 8;
+ ```
+
#### Precautions after the upgrade
The alert plugin may have some dirty data. After the upgrade, clear it
manually by referring to SQL.
diff --git a/docs/docs/zh/architecture/configuration.md
b/docs/docs/zh/architecture/configuration.md
index a2b9437955..00012cf0ea 100644
--- a/docs/docs/zh/architecture/configuration.md
+++ b/docs/docs/zh/architecture/configuration.md
@@ -223,6 +223,9 @@ common.properties配置文件目前主要是配置hadoop/s3/yarn/applicationId
| sudo.enable | true
| 是否开启sudo
|
| zeppelin.rest.url | http://localhost:8080
| zeppelin RESTful API 接口地址
|
| appId.collect | log
| 收集applicationId方式,
如果用aop方法,将配置log替换为aop,并将`bin/env/dolphinscheduler_env.sh`自动收集applicationId相关环境变量配置的注释取消掉,注意:aop不支持远程主机提交yarn作业的方式比如Beeline客户端提交,且如果用户环境覆盖了dolphinscheduler_env.sh收集applicationId相关环境变量配置,aop方法会失效
|
+| sql.task.alert.max.rows | 30
| SQL 任务结果告警内容最多保留的行数,超出的行以省略标记表示。修改后需重启 Worker Server 生效
|
+| sql.task.alert.max.row.value.length | 100
| SQL 任务结果告警内容中单个字符串字段值最多保留的字符数,超出的值以省略号截断。修改后需重启 Worker
Server 生效
|
+| sql.task.alert.max.content.bytes | 60000
| SQL 任务结果告警内容序列化后的最大 UTF-8 字节数,小于存储告警内容的 MySQL TEXT 列
65535 字节容量。修改后需重启 Worker Server 生效
|
## Api-server相关配置
diff --git a/docs/docs/zh/guide/upgrade/incompatible.md
b/docs/docs/zh/guide/upgrade/incompatible.md
index a0f9455019..7e3de19628 100644
--- a/docs/docs/zh/guide/upgrade/incompatible.md
+++ b/docs/docs/zh/guide/upgrade/incompatible.md
@@ -56,3 +56,8 @@
* **移除的派生属性**:`cmdTypeIfComplement`、`complementData`(补数执行相关,如需获取请使用详情接口)
* 如需获取这些字段,请使用详情接口 `GET
/projects/{projectCode}/workflow-instances/{id}`,该接口仍返回完整的 `WorkflowInstance`
对象 ([#18444](https://github.com/apache/dolphinscheduler/pull/18444))
+## 3.5.0
+
+* `AlertSender#getAlertData` 对 null `alertType` 增加了显式拒绝(抛
`IllegalArgumentException` 并记录错误日志),而非直接抛 `NullPointerException`。滚动升级时**必须先升级
Alert Server,再升级 Master/Worker**,否则旧 Alert Server 无法识别新增的 `alert_type` 枚举值(如
`TASK_RESULT`),会导致告警投递失败。详见[升级文档](upgrade.md#滚动升降级顺序)。([#18549](https://github.com/apache/dolphinscheduler/pull/18549))
+* SQL 任务结果告警内容有大小限制:默认最多保留 30 行,超过 100 字符的字符串字段值会被截断并以省略号标记;序列化后内容总大小限制在 60000
UTF-8 字节以内(兼容 MySQL TEXT 列 65535 字节容量),超出预算的行以省略标记对象表示。以上默认值可通过
`common.properties` 中的
`sql.task.alert.max.rows`、`sql.task.alert.max.row.value.length`、`sql.task.alert.max.content.bytes`
配置调整,修改后需重启 Worker 生效。
([#18549](https://github.com/apache/dolphinscheduler/pull/18549))
+
diff --git a/docs/docs/zh/guide/upgrade/upgrade.md
b/docs/docs/zh/guide/upgrade/upgrade.md
index 19ccad73ca..0e5221442e 100644
--- a/docs/docs/zh/guide/upgrade/upgrade.md
+++ b/docs/docs/zh/guide/upgrade/upgrade.md
@@ -76,6 +76,28 @@ jar 包 并添加到 `./tools/libs` 目录下,设置以下环境变量
- 在 3.3.X 以及之后的版本,我们仅支持从 3.0.0 开始进行升级,低于此版本的请下载历史版本升级至 3.0.0。
- 在 3.3.X
以及之后的版本,二进制包不再默认提供插件依赖,因此第一次使用时,需要自行下载安装。具体请参考请参照[伪集群部署(Pseudo-Cluster)](../installation/pseudo-cluster.md)
+#### 滚动升降级顺序
+
+在滚动升降级场景下,**必须先升级 Alert Server,再升级 Master/Worker**。
+
+从 3.5.0 起,Master 可能向 `t_ds_alert` 表写入新的 `alert_type` 枚举值(如 `TASK_RESULT`)。如果
Alert Server 尚未升级到包含该枚举值的版本,MyBatis 会将未知的枚举值反序列化为 `null`,导致 `AlertSender`
在构建告警数据时抛出 `NullPointerException`,告警将卡在 `WAIT_EXECUTION` 状态无法投递。
+
+按照"Alert Server → Master → Worker"的顺序升级可消除此风险。
+
+#### 如果升级失败
+
+如果未按上述顺序升级(例如在 Alert Server 之前先升级了 Master/Worker),且新的 `alert_type` 枚举值已写入
`t_ds_alert` 表,请按以下步骤恢复:
+
+1. 将 Alert Server 升级到包含新枚举值的版本并重启,卡在 `WAIT_EXECUTION` 状态的告警会被正常拉取并投递。
+2. 如果暂时无法升级 Alert Server,请先停止旧版 Alert Server,再备份并清理受影响告警,避免其持续阻塞告警投递:
+
+ ```sql
+ -- 1. 备份受影响告警
+ create table t_ds_alert_bak as select * from t_ds_alert where alert_type =
8;
+ -- 2. 从主表清理
+ delete from t_ds_alert where alert_type = 8;
+ ```
+
#### 升级后的注意事项
在历史版本中可能告警插件会有一些脏数据,升级后请参考一下 SQL 手动清理。
diff --git
a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSender.java
b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSender.java
index 9c9cd034bd..2f829e5bcd 100644
---
a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSender.java
+++
b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSender.java
@@ -100,6 +100,13 @@ public class AlertSender extends
AbstractEventSender<Alert> {
@Override
public AlertData getAlertData(Alert event) {
+ if (event.getAlertType() == null) {
+ log.error("Alert {} has null alertType, refusing to send. This
indicates the Alert Server version "
+ + "is older than the Master that created the alert. Please
upgrade the Alert Server first "
+ + "(see upgrade guide: Alert Server -> Master ->
Worker).", event.getId());
+ throw new IllegalArgumentException(
+ "Cannot build AlertData with null alertType for alert " +
event.getId());
+ }
return AlertData.builder()
.id(event.getId())
.content(event.getContent())
diff --git
a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertSenderTest.java
b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertSenderTest.java
index 18246f485a..2e8f5b2672 100644
---
a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertSenderTest.java
+++
b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertSenderTest.java
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.apache.dolphinscheduler.alert.api.AlertChannel;
+import org.apache.dolphinscheduler.alert.api.AlertData;
import org.apache.dolphinscheduler.alert.api.AlertResult;
import org.apache.dolphinscheduler.alert.config.AlertConfig;
import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager;
@@ -210,4 +211,33 @@ class AlertSenderTest {
.thenReturn(paramsMap);
alertSender.syncTestSend(PLUGIN_DEFINE_ID, PLUGIN_INSTANCE_PARAMS);
}
+
+ @Test
+ void testGetAlertDataWithNullAlertType() {
+ Alert alert = new Alert();
+ alert.setId(1);
+ alert.setTitle(TITLE);
+ alert.setContent(CONTENT);
+ alert.setAlertGroupId(ALERT_GROUP_ID);
+ alert.setWarningType(WarningType.FAILURE);
+ alert.setAlertType(null);
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> alertSender.getAlertData(alert));
+ }
+
+ @Test
+ void testGetAlertDataWithValidAlertType() {
+ Alert alert = new Alert();
+ alert.setId(2);
+ alert.setTitle(TITLE);
+ alert.setContent(CONTENT);
+ alert.setAlertGroupId(ALERT_GROUP_ID);
+ alert.setWarningType(WarningType.FAILURE);
+ alert.setAlertType(AlertType.TASK_RESULT);
+
+ AlertData alertData = alertSender.getAlertData(alert);
+ Assertions.assertNotNull(alertData);
+ Assertions.assertEquals(AlertType.TASK_RESULT.getCode(),
alertData.getAlertType());
+ }
}
diff --git
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java
index 81783ed285..e5815701df 100644
---
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java
+++
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java
@@ -59,6 +59,23 @@ public final class Constants {
*/
public static final String SHELL_KILL_WAIT_TIMEOUT =
"shell.kill.wait.timeout";
+ /**
+ * sql.task.alert.max.rows: this property defines the max rows kept in the
task-result alert content.
+ */
+ public static final String SQL_TASK_ALERT_MAX_ROWS =
"sql.task.alert.max.rows";
+
+ /**
+ * sql.task.alert.max.row.value.length: this property defines the max
chars of a single string value kept in the
+ * task-result alert content.
+ */
+ public static final String SQL_TASK_ALERT_MAX_ROW_VALUE_LENGTH =
"sql.task.alert.max.row.value.length";
+
+ /**
+ * sql.task.alert.max.content.bytes: this property defines the max
serialized UTF-8 bytes of the task-result alert
+ * content.
+ */
+ public static final String SQL_TASK_ALERT_MAX_CONTENT_BYTES =
"sql.task.alert.max.content.bytes";
+
public static final String SUDO_ENABLE = "sudo.enable";
public static final String RESOURCE_STORAGE_TYPE = "resource.storage.type";
diff --git
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AlertType.java
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AlertType.java
index 058afcb3fc..6cc850d6f1 100644
---
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AlertType.java
+++
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AlertType.java
@@ -29,7 +29,7 @@ public enum AlertType {
/**
* 0 workflow instance failure, 1 workflow instance success, 2 workflow
instance blocked, 3 workflow instance timeout, 4 fault tolerance warning,
- * 5 task failure, 6 task success, 7 task timeout
+ * 5 task failure, 6 task success, 7 task timeout, 8 task result
*/
WORKFLOW_INSTANCE_FAILURE(0, "workflow instance failure"),
WORKFLOW_INSTANCE_SUCCESS(1, "workflow instance success"),
@@ -39,6 +39,7 @@ public enum AlertType {
TASK_FAILURE(5, "task failure"),
TASK_SUCCESS(6, "task success"),
TASK_TIMEOUT(7, "task timeout"),
+ TASK_RESULT(8, "task result"),
;
AlertType(int code, String descp) {
diff --git a/dolphinscheduler-common/src/main/resources/common.properties
b/dolphinscheduler-common/src/main/resources/common.properties
index 9e5adea62e..fa397d7ce9 100644
--- a/dolphinscheduler-common/src/main/resources/common.properties
+++ b/dolphinscheduler-common/src/main/resources/common.properties
@@ -108,3 +108,11 @@ shell.interceptor.type=bash
# Whether to enable remote logging
remote.logging.enable=false
+
+# SQL task-result alert content limits, take effect on worker server restart:
+# sql.task.alert.max.rows: max rows kept in the alert content, default: 30
+# sql.task.alert.max.row.value.length: max chars of a single string value
kept in the alert content, default: 100
+# sql.task.alert.max.content.bytes: max serialized UTF-8 bytes of the alert
content, default: 60000
+sql.task.alert.max.rows=30
+sql.task.alert.max.row.value.length=100
+sql.task.alert.max.content.bytes=60000
diff --git a/dolphinscheduler-common/src/test/resources/common.properties
b/dolphinscheduler-common/src/test/resources/common.properties
index f849b1c3e0..99ec046b94 100644
--- a/dolphinscheduler-common/src/test/resources/common.properties
+++ b/dolphinscheduler-common/src/test/resources/common.properties
@@ -201,4 +201,10 @@
remote.logging.google.cloud.storage.credential=/path/to/credential
# gcs bucket name, required if you set remote.logging.target=GCS
remote.logging.google.cloud.storage.bucket.name=<your-bucket>
-
+# SQL task-result alert content limits, take effect on worker server restart:
+# sql.task.alert.max.rows: max rows kept in the alert content, default: 30
+# sql.task.alert.max.row.value.length: max chars of a single string value
kept in the alert content, default: 100
+# sql.task.alert.max.content.bytes: max serialized UTF-8 bytes of the alert
content, default: 60000
+sql.task.alert.max.rows=30
+sql.task.alert.max.row.value.length=100
+sql.task.alert.max.content.bytes=60000
diff --git
a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/AlertDao.java
b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/AlertDao.java
index 19ed2d70f2..f5a4ab3c48 100644
---
a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/AlertDao.java
+++
b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/AlertDao.java
@@ -108,6 +108,24 @@ public class AlertDao {
return count;
}
+ /**
+ * Insert a task-result alert.
+ *
+ * @return insert count
+ */
+ public int addTaskResultAlert(Alert alert) {
+ if (null == alert.getAlertGroupId() ||
NumberUtils.INTEGER_ZERO.equals(alert.getAlertGroupId())) {
+ log.warn("the value of alertGroupId is null or 0 ");
+ return 0;
+ }
+
+ String sign = generateSign(alert);
+ alert.setSign(sign);
+ int count = alertMapper.insert(alert);
+ log.info("add task result alert to db , alert: {}", alert);
+ return count;
+ }
+
/**
* update alert sending(execution) status
*
@@ -132,6 +150,13 @@ public class AlertDao {
* @return sign's str
*/
private String generateSign(Alert alert) {
+ // Task-result alerts include the task instance ID in the sign so that
different
+ // tasks returning identical results get different signs, and repeated
success
+ // events of the same task yield the same sign. For other alert types
+ // taskInstanceId is null and the sign falls back to content-only.
+ if (alert.getTaskInstanceId() != null) {
+ return DigestUtils.sha1Hex(alert.getTaskInstanceId() + "|" +
alert.getContent()).toLowerCase();
+ }
return Optional.of(alert)
.map(Alert::getContent)
.map(DigestUtils::sha1Hex)
diff --git
a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Alert.java
b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Alert.java
index 96ad2890ba..5916bedc37 100644
---
a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Alert.java
+++
b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Alert.java
@@ -84,6 +84,12 @@ public class Alert {
@TableField("alert_type")
private AlertType alertType;
+ /**
+ * Transient — used only for sign generation in idempotent task-result
alert inserts.
+ */
+ @TableField(exist = false)
+ private Integer taskInstanceId;
+
@TableField(exist = false)
private Map<String, Object> info = new HashMap<>();
diff --git
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/event/TaskSuccessLifecycleEvent.java
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/event/TaskSuccessLifecycleEvent.java
index 9c7f41e1b5..4568768bca 100644
---
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/event/TaskSuccessLifecycleEvent.java
+++
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/event/TaskSuccessLifecycleEvent.java
@@ -18,6 +18,7 @@
package org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event;
import org.apache.dolphinscheduler.plugin.task.api.model.Property;
+import org.apache.dolphinscheduler.plugin.task.api.model.TaskAlertInfo;
import org.apache.dolphinscheduler.server.master.engine.ILifecycleEventType;
import
org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution;
import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.AbstractTaskLifecycleEvent;
@@ -41,6 +42,10 @@ public class TaskSuccessLifecycleEvent extends
AbstractTaskLifecycleEvent {
private final List<Property> varPool;
+ private final boolean needAlert;
+
+ private final TaskAlertInfo taskAlertInfo;
+
@Override
public ILifecycleEventType getEventType() {
return TaskLifecycleEventType.SUCCEEDED;
@@ -52,6 +57,7 @@ public class TaskSuccessLifecycleEvent extends
AbstractTaskLifecycleEvent {
"task=" + taskExecution.getName() +
", endTime=" + endTime +
", varPool='" + varPool + '\'' +
+ ", needAlert=" + needAlert +
'}';
}
}
diff --git
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/handler/TaskSuccessLifecycleEventHandler.java
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/handler/TaskSuccessLifecycleEventHandler.java
index 86a6ec1799..d9e9f88f5f 100644
---
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/handler/TaskSuccessLifecycleEventHandler.java
+++
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/handler/TaskSuccessLifecycleEventHandler.java
@@ -17,6 +17,7 @@
package
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler;
+import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
import org.apache.dolphinscheduler.server.master.engine.ILifecycleEventType;
import
org.apache.dolphinscheduler.server.master.engine.task.client.TaskExecutorClient;
import
org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution;
@@ -24,18 +25,26 @@ import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.TaskLifec
import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskSuccessLifecycleEvent;
import
org.apache.dolphinscheduler.server.master.engine.task.statemachine.ITaskStateAction;
import
org.apache.dolphinscheduler.server.master.engine.workflow.execution.IWorkflowExecution;
+import org.apache.dolphinscheduler.service.alert.WorkflowAlertManager;
import
org.apache.dolphinscheduler.task.executor.eventbus.ITaskExecutorLifecycleEventReporter;
import
org.apache.dolphinscheduler.task.executor.events.TaskExecutorLifecycleEventType;
+import lombok.extern.slf4j.Slf4j;
+
import org.springframework.stereotype.Component;
+@Slf4j
@Component
public class TaskSuccessLifecycleEventHandler extends
AbstractTaskLifecycleEventHandler<TaskSuccessLifecycleEvent> {
private final TaskExecutorClient taskExecutorClient;
- public TaskSuccessLifecycleEventHandler(final TaskExecutorClient
taskExecutorClient) {
+ private final WorkflowAlertManager workflowAlertManager;
+
+ public TaskSuccessLifecycleEventHandler(final TaskExecutorClient
taskExecutorClient,
+ final WorkflowAlertManager
workflowAlertManager) {
this.taskExecutorClient = taskExecutorClient;
+ this.workflowAlertManager = workflowAlertManager;
}
@Override
@@ -44,6 +53,19 @@ public class TaskSuccessLifecycleEventHandler extends
AbstractTaskLifecycleEvent
final ITaskExecution taskExecution,
final TaskSuccessLifecycleEvent taskSuccessEvent) {
taskStateAction.onSucceedEvent(workflowExecution, taskExecution,
taskSuccessEvent);
+
+ // Only persist the task result alert when the success event has been
accepted and the
+ // task has actually transitioned to SUCCESS. State actions that
reject a late success
+ // event (e.g. a paused or killed task) only log a warning and keep
the task in its
+ // current state, so the alert must be skipped as well.
+ if (taskExecution.getTaskInstance().getState() ==
TaskExecutionStatus.SUCCESS
+ && taskSuccessEvent.isNeedAlert()) {
+ workflowAlertManager.sendTaskResultAlert(
+ taskExecution.getWorkflowInstance(),
+ taskExecution.getTaskInstance(),
+ taskSuccessEvent.getTaskAlertInfo());
+ }
+
taskExecutorClient.ackTaskExecutorLifecycleEvent(
taskExecution,
new
ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck(
diff --git
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/rpc/TaskExecutorEventListenerImpl.java
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/rpc/TaskExecutorEventListenerImpl.java
index 278f5160dc..0566c83985 100644
---
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/rpc/TaskExecutorEventListenerImpl.java
+++
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/rpc/TaskExecutorEventListenerImpl.java
@@ -116,6 +116,8 @@ public class TaskExecutorEventListenerImpl implements
ITaskExecutorEventListener
.taskExecution(taskExecution)
.endTime(new
Date(taskExecutorSuccessLifecycleEvent.getEndTime()))
.varPool(taskExecutorSuccessLifecycleEvent.getVarPool())
+ .needAlert(taskExecutorSuccessLifecycleEvent.isNeedAlert())
+
.taskAlertInfo(taskExecutorSuccessLifecycleEvent.getTaskAlertInfo())
.build();
taskExecution.getWorkflowEventBus().publish(taskSuccessEvent);
} finally {
diff --git
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/handler/TaskSuccessLifecycleEventHandlerTest.java
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/handler/TaskSuccessLifecycleEventHandlerTest.java
new file mode 100644
index 0000000000..8545becfd9
--- /dev/null
+++
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/lifecycle/handler/TaskSuccessLifecycleEventHandlerTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.dolphinscheduler.server.master.engine.task.lifecycle.handler;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import org.apache.dolphinscheduler.dao.entity.TaskInstance;
+import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
+import org.apache.dolphinscheduler.plugin.task.api.model.TaskAlertInfo;
+import
org.apache.dolphinscheduler.server.master.engine.task.client.TaskExecutorClient;
+import
org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskSuccessLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.statemachine.ITaskStateAction;
+import
org.apache.dolphinscheduler.server.master.engine.workflow.execution.IWorkflowExecution;
+import org.apache.dolphinscheduler.service.alert.WorkflowAlertManager;
+import
org.apache.dolphinscheduler.task.executor.eventbus.ITaskExecutorLifecycleEventReporter;
+
+import java.util.Collections;
+import java.util.Date;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class TaskSuccessLifecycleEventHandlerTest {
+
+ @Mock
+ private TaskExecutorClient taskExecutorClient;
+
+ @Mock
+ private WorkflowAlertManager workflowAlertManager;
+
+ @Mock
+ private ITaskStateAction taskStateAction;
+
+ @Mock
+ private ITaskExecution taskExecution;
+
+ @Mock
+ private IWorkflowExecution workflowExecution;
+
+ private TaskInstance taskInstance;
+
+ private TaskSuccessLifecycleEventHandler taskSuccessLifecycleEventHandler;
+
+ @BeforeEach
+ void setUp() {
+ taskSuccessLifecycleEventHandler =
+ new TaskSuccessLifecycleEventHandler(taskExecutorClient,
workflowAlertManager);
+
+ taskInstance = new TaskInstance();
+ taskInstance.setState(TaskExecutionStatus.RUNNING_EXECUTION);
+ taskInstance.setHost("localhost");
+
+ when(taskExecution.getTaskInstance()).thenReturn(taskInstance);
+ when(taskExecution.getId()).thenReturn(1);
+ }
+
+ @Test
+ void shouldSendResultAlertWhenSucceedEventTransitionsTaskToSuccess() {
+ doAnswer(invocation -> {
+ taskInstance.setState(TaskExecutionStatus.SUCCESS);
+ return null;
+ }).when(taskStateAction)
+ .onSucceedEvent(any(), any(), any());
+
+ final TaskSuccessLifecycleEvent taskSuccessEvent =
newSuccessEvent(true);
+
+ taskSuccessLifecycleEventHandler.handle(
+ taskStateAction, workflowExecution, taskExecution,
taskSuccessEvent);
+
+ verify(workflowAlertManager).sendTaskResultAlert(
+ any(), eq(taskInstance), any());
+ verify(taskExecutorClient).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ }
+
+ @Test
+ void shouldNotSendResultAlertWhenSucceedEventArrivesOnPausedTask() {
+ taskInstance.setState(TaskExecutionStatus.PAUSE);
+
+ final TaskSuccessLifecycleEvent taskSuccessEvent =
newSuccessEvent(true);
+
+ taskSuccessLifecycleEventHandler.handle(
+ taskStateAction, workflowExecution, taskExecution,
taskSuccessEvent);
+
+ verifyNoInteractions(workflowAlertManager);
+ verify(taskExecutorClient).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ }
+
+ @Test
+ void shouldNotSendResultAlertWhenSucceedEventArrivesOnKilledTask() {
+ taskInstance.setState(TaskExecutionStatus.KILL);
+
+ final TaskSuccessLifecycleEvent taskSuccessEvent =
newSuccessEvent(true);
+
+ taskSuccessLifecycleEventHandler.handle(
+ taskStateAction, workflowExecution, taskExecution,
taskSuccessEvent);
+
+ verifyNoInteractions(workflowAlertManager);
+ verify(taskExecutorClient).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ }
+
+ @Test
+ void shouldStillSendResultAlertForRepeatedSucceedEvent() {
+ taskInstance.setState(TaskExecutionStatus.SUCCESS);
+
+ final TaskSuccessLifecycleEvent taskSuccessEvent =
newSuccessEvent(true);
+
+ taskSuccessLifecycleEventHandler.handle(
+ taskStateAction, workflowExecution, taskExecution,
taskSuccessEvent);
+
+ // Repeated success events are accepted idempotently: the alert is
sent again,
+ // which may insert a duplicate row; occasional duplicates are
accepted.
+ verify(workflowAlertManager).sendTaskResultAlert(
+ any(), eq(taskInstance), any());
+ verify(taskExecutorClient).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ }
+
+ @Test
+ void shouldNotSendResultAlertWhenNeedAlertIsFalse() {
+ doAnswer(invocation -> {
+ taskInstance.setState(TaskExecutionStatus.SUCCESS);
+ return null;
+ }).when(taskStateAction)
+ .onSucceedEvent(any(), any(), any());
+
+ final TaskSuccessLifecycleEvent taskSuccessEvent =
newSuccessEvent(false);
+
+ taskSuccessLifecycleEventHandler.handle(
+ taskStateAction, workflowExecution, taskExecution,
taskSuccessEvent);
+
+ verifyNoInteractions(workflowAlertManager);
+ verify(taskExecutorClient).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ }
+
+ private TaskSuccessLifecycleEvent newSuccessEvent(final boolean needAlert)
{
+ return TaskSuccessLifecycleEvent.builder()
+ .taskExecution(taskExecution)
+ .endTime(new Date())
+ .varPool(Collections.emptyList())
+ .needAlert(needAlert)
+ .taskAlertInfo(new TaskAlertInfo())
+ .build();
+ }
+}
diff --git
a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/alert/WorkflowAlertManager.java
b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/alert/WorkflowAlertManager.java
index 9335e9551e..cba5179b6e 100644
---
a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/alert/WorkflowAlertManager.java
+++
b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/alert/WorkflowAlertManager.java
@@ -17,6 +17,7 @@
package org.apache.dolphinscheduler.service.alert;
+import org.apache.dolphinscheduler.common.enums.AlertStatus;
import org.apache.dolphinscheduler.common.enums.AlertType;
import org.apache.dolphinscheduler.common.enums.CommandType;
import org.apache.dolphinscheduler.common.enums.Flag;
@@ -34,6 +35,7 @@ import
org.apache.dolphinscheduler.dao.entity.WorkflowInstance;
import org.apache.dolphinscheduler.dao.repository.ProjectDao;
import org.apache.dolphinscheduler.dao.repository.UserDao;
import org.apache.dolphinscheduler.dao.repository.WorkflowDefinitionLogDao;
+import org.apache.dolphinscheduler.plugin.task.api.model.TaskAlertInfo;
import java.util.ArrayList;
import java.util.Date;
@@ -208,4 +210,30 @@ public class WorkflowAlertManager {
alertDao.sendWorkflowTimeoutAlert(workflowInstance, projectUser);
}
+ public void sendTaskResultAlert(WorkflowInstance workflowInstance,
+ TaskInstance taskInstance,
+ TaskAlertInfo taskAlertInfo) {
+ if (taskAlertInfo == null || taskAlertInfo.getAlertGroupId() == null
+ || taskAlertInfo.getAlertGroupId() <= 0) {
+ return;
+ }
+
+ Alert alert = new Alert();
+ alert.setTitle(taskAlertInfo.getTitle());
+ alert.setContent(taskAlertInfo.getContent());
+ alert.setWarningType(WarningType.SUCCESS);
+ alert.setAlertStatus(AlertStatus.WAIT_EXECUTION);
+ alert.setCreateTime(new Date());
+ alert.setAlertGroupId(taskAlertInfo.getAlertGroupId());
+ alert.setProjectCode(workflowInstance.getProjectCode());
+
alert.setWorkflowDefinitionCode(workflowInstance.getWorkflowDefinitionCode());
+ alert.setWorkflowInstanceId(workflowInstance.getId());
+ alert.setAlertType(taskAlertInfo.getAlertType() != null
+ ? taskAlertInfo.getAlertType()
+ : AlertType.TASK_RESULT);
+ alert.setTaskInstanceId(taskInstance.getId());
+ alertDao.addTaskResultAlert(alert);
+ log.info("Send task result alert for task: {} in workflow: {}",
+ taskInstance.getName(), workflowInstance.getName());
+ }
}
diff --git
a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/alert/WorkflowAlertManagerTest.java
b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/alert/WorkflowAlertManagerTest.java
new file mode 100644
index 0000000000..0fcdfe0cbc
--- /dev/null
+++
b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/alert/WorkflowAlertManagerTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.dolphinscheduler.service.alert;
+
+import org.apache.dolphinscheduler.common.enums.AlertStatus;
+import org.apache.dolphinscheduler.common.enums.AlertType;
+import org.apache.dolphinscheduler.common.enums.WarningType;
+import org.apache.dolphinscheduler.dao.AlertDao;
+import org.apache.dolphinscheduler.dao.entity.Alert;
+import org.apache.dolphinscheduler.dao.entity.TaskInstance;
+import org.apache.dolphinscheduler.dao.entity.WorkflowInstance;
+import org.apache.dolphinscheduler.dao.repository.ProjectDao;
+import org.apache.dolphinscheduler.dao.repository.UserDao;
+import org.apache.dolphinscheduler.dao.repository.WorkflowDefinitionLogDao;
+import org.apache.dolphinscheduler.plugin.task.api.model.TaskAlertInfo;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class WorkflowAlertManagerTest {
+
+ @Mock
+ private AlertDao alertDao;
+
+ @Mock
+ private WorkflowDefinitionLogDao workflowDefinitionLogDao;
+
+ @Mock
+ private UserDao userDao;
+
+ @Mock
+ private ProjectDao projectDao;
+
+ @InjectMocks
+ private WorkflowAlertManager workflowAlertManager;
+
+ @Test
+ void testSendTaskResultAlertSetsWaitExecutionStatus() {
+ WorkflowInstance workflowInstance = new WorkflowInstance();
+ workflowInstance.setId(1001);
+ workflowInstance.setName("test-workflow");
+ workflowInstance.setProjectCode(2001L);
+ workflowInstance.setWorkflowDefinitionCode(3001L);
+
+ TaskInstance taskInstance = new TaskInstance();
+ taskInstance.setId(5001);
+ taskInstance.setName("sql-query-task");
+
+ TaskAlertInfo taskAlertInfo = new TaskAlertInfo();
+ taskAlertInfo.setTitle("SQL Task Result");
+
taskAlertInfo.setContent("[{\"taskName\":\"sql-query-task\",\"result\":\"ok\"}]");
+ taskAlertInfo.setAlertGroupId(1);
+ taskAlertInfo.setAlertType(AlertType.TASK_RESULT);
+
+
Mockito.when(alertDao.addTaskResultAlert(Mockito.any(Alert.class))).thenReturn(1);
+
+ workflowAlertManager.sendTaskResultAlert(workflowInstance,
taskInstance, taskAlertInfo);
+
+ ArgumentCaptor<Alert> alertCaptor =
ArgumentCaptor.forClass(Alert.class);
+ Mockito.verify(alertDao).addTaskResultAlert(alertCaptor.capture());
+
+ Alert captured = alertCaptor.getValue();
+ Assertions.assertEquals(AlertStatus.WAIT_EXECUTION,
captured.getAlertStatus());
+ Assertions.assertEquals("SQL Task Result", captured.getTitle());
+ Assertions.assertEquals(WarningType.SUCCESS,
captured.getWarningType());
+ Assertions.assertEquals(AlertType.TASK_RESULT,
captured.getAlertType());
+ Assertions.assertEquals(1001, captured.getWorkflowInstanceId());
+ Assertions.assertEquals(2001L, captured.getProjectCode());
+ Assertions.assertEquals(3001L, captured.getWorkflowDefinitionCode());
+ Assertions.assertEquals(1, captured.getAlertGroupId());
+ Assertions.assertNotNull(captured.getCreateTime());
+ Assertions.assertEquals(5001, captured.getTaskInstanceId());
+ }
+
+ @Test
+ void testSendTaskResultAlertNullTaskAlertInfoDoesNothing() {
+ WorkflowInstance workflowInstance = new WorkflowInstance();
+ workflowInstance.setId(1001);
+ TaskInstance taskInstance = new TaskInstance();
+
+ workflowAlertManager.sendTaskResultAlert(workflowInstance,
taskInstance, null);
+
+ Mockito.verifyNoInteractions(alertDao);
+ }
+
+ @Test
+ void testSendTaskResultAlertNullAlertGroupIdDoesNothing() {
+ WorkflowInstance workflowInstance = new WorkflowInstance();
+ workflowInstance.setId(1001);
+ TaskInstance taskInstance = new TaskInstance();
+
+ TaskAlertInfo taskAlertInfo = new TaskAlertInfo();
+ taskAlertInfo.setTitle("SQL Task Result");
+ taskAlertInfo.setContent("[]");
+ taskAlertInfo.setAlertGroupId(null);
+
+ workflowAlertManager.sendTaskResultAlert(workflowInstance,
taskInstance, taskAlertInfo);
+
+ Mockito.verifyNoInteractions(alertDao);
+ }
+}
diff --git
a/dolphinscheduler-task-executor/src/main/java/org/apache/dolphinscheduler/task/executor/events/TaskExecutorSuccessLifecycleEvent.java
b/dolphinscheduler-task-executor/src/main/java/org/apache/dolphinscheduler/task/executor/events/TaskExecutorSuccessLifecycleEvent.java
index 947d1edf36..2c55c80c36 100644
---
a/dolphinscheduler-task-executor/src/main/java/org/apache/dolphinscheduler/task/executor/events/TaskExecutorSuccessLifecycleEvent.java
+++
b/dolphinscheduler-task-executor/src/main/java/org/apache/dolphinscheduler/task/executor/events/TaskExecutorSuccessLifecycleEvent.java
@@ -19,6 +19,7 @@ package org.apache.dolphinscheduler.task.executor.events;
import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext;
import org.apache.dolphinscheduler.plugin.task.api.model.Property;
+import org.apache.dolphinscheduler.plugin.task.api.model.TaskAlertInfo;
import org.apache.dolphinscheduler.task.executor.ITaskExecutor;
import java.util.List;
@@ -48,6 +49,10 @@ public class TaskExecutorSuccessLifecycleEvent extends
AbstractTaskExecutorLifec
private Long latestReportTime;
+ private boolean needAlert;
+
+ private TaskAlertInfo taskAlertInfo;
+
public static TaskExecutorSuccessLifecycleEvent of(final ITaskExecutor
taskExecutor) {
final TaskExecutionContext taskExecutionContext =
taskExecutor.getTaskExecutionContext();
return TaskExecutorSuccessLifecycleEvent.builder()
@@ -57,6 +62,8 @@ public class TaskExecutorSuccessLifecycleEvent extends
AbstractTaskExecutorLifec
.varPool(taskExecutionContext.getVarPool())
.type(TaskExecutorLifecycleEventType.SUCCESS)
.endTime(taskExecutionContext.getEndTime())
+ .needAlert(taskExecutionContext.isNeedAlert())
+ .taskAlertInfo(taskExecutionContext.getTaskAlertInfo())
.build();
}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTask.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTask.java
index 0c8f208fcb..afa990e7bb 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTask.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTask.java
@@ -56,8 +56,22 @@ public abstract class AbstractTask {
*/
protected volatile int exitStatusCode = -1;
+ /**
+ * @deprecated Use {@link TaskExecutionContext#isNeedAlert()} on the
+ * {@code taskRequest} field instead. Retained for binary
+ * compatibility with third-party plugins compiled against
+ * the previous AbstractTask API.
+ */
+ @Deprecated
protected boolean needAlert = false;
+ /**
+ * @deprecated Use {@link TaskExecutionContext#getTaskAlertInfo()} on the
+ * {@code taskRequest} field instead. Retained for binary
+ * compatibility with third-party plugins compiled against
+ * the previous AbstractTask API.
+ */
+ @Deprecated
protected TaskAlertInfo taskAlertInfo;
/**
@@ -109,20 +123,53 @@ public abstract class AbstractTask {
this.appIds = appIds;
}
+ /**
+ * @deprecated Use {@link TaskExecutionContext#isNeedAlert()} on the
+ * {@code taskRequest} field instead. This method is retained
as a
+ * compatibility bridge so that third-party plugins compiled
against
+ * the previous AbstractTask API continue to work. The value
is merged
+ * with the legacy field so that direct field writes from such
plugins
+ * are still honored.
+ */
+ @Deprecated
public boolean getNeedAlert() {
- return needAlert;
+ return needAlert || (taskRequest != null && taskRequest.isNeedAlert());
}
+ /**
+ * @deprecated Use {@link TaskExecutionContext#setNeedAlert(boolean)} on
the
+ * {@code taskRequest} field instead.
+ */
+ @Deprecated
public void setNeedAlert(boolean needAlert) {
this.needAlert = needAlert;
+ if (taskRequest != null) {
+ taskRequest.setNeedAlert(needAlert);
+ }
}
+ /**
+ * @deprecated Use {@link TaskExecutionContext#getTaskAlertInfo()} on the
+ * {@code taskRequest} field instead.
+ */
+ @Deprecated
public TaskAlertInfo getTaskAlertInfo() {
- return taskAlertInfo;
+ if (taskAlertInfo != null) {
+ return taskAlertInfo;
+ }
+ return taskRequest != null ? taskRequest.getTaskAlertInfo() : null;
}
+ /**
+ * @deprecated Use {@link
TaskExecutionContext#setTaskAlertInfo(TaskAlertInfo)}
+ * on the {@code taskRequest} field instead.
+ */
+ @Deprecated
public void setTaskAlertInfo(TaskAlertInfo taskAlertInfo) {
this.taskAlertInfo = taskAlertInfo;
+ if (taskRequest != null) {
+ taskRequest.setTaskAlertInfo(taskAlertInfo);
+ }
}
/**
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/TaskExecutionContext.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/TaskExecutionContext.java
index 85d6f68219..a072258011 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/TaskExecutionContext.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/TaskExecutionContext.java
@@ -19,6 +19,7 @@ package org.apache.dolphinscheduler.plugin.task.api;
import org.apache.dolphinscheduler.plugin.task.api.enums.TaskTimeoutStrategy;
import org.apache.dolphinscheduler.plugin.task.api.model.Property;
+import org.apache.dolphinscheduler.plugin.task.api.model.TaskAlertInfo;
import
org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper;
import org.apache.dolphinscheduler.plugin.task.api.resource.ResourceContext;
@@ -130,6 +131,10 @@ public class TaskExecutionContext implements Serializable {
private final long firstDispatchTime = System.currentTimeMillis();
+ private boolean needAlert;
+
+ private TaskAlertInfo taskAlertInfo;
+
public int increaseDispatchFailTimes() {
return ++dispatchFailTimes;
}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/model/TaskAlertInfo.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/model/TaskAlertInfo.java
index e72242e934..d2c22e565a 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/model/TaskAlertInfo.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/model/TaskAlertInfo.java
@@ -17,6 +17,8 @@
package org.apache.dolphinscheduler.plugin.task.api.model;
+import org.apache.dolphinscheduler.common.enums.AlertType;
+
import lombok.Data;
@Data
@@ -27,4 +29,7 @@ public class TaskAlertInfo {
private String content;
private Integer alertGroupId;
+
+ private AlertType alertType;
+
}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTaskTest.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTaskTest.java
index 5421c05575..f3a8a8b534 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTaskTest.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/test/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTaskTest.java
@@ -18,6 +18,7 @@
package org.apache.dolphinscheduler.plugin.task.api;
import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
+import org.apache.dolphinscheduler.plugin.task.api.model.TaskAlertInfo;
import
org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters;
import
org.apache.dolphinscheduler.plugin.task.api.parameters.SubWorkflowParameters;
@@ -62,12 +63,65 @@ public class AbstractTaskTest {
Assertions.assertEquals(TaskExecutionStatus.FAILURE,
task.getExitStatus());
}
+ @Test
+ public void testDirectFieldAssignmentIsExposedByGetters() {
+ AbstractTask task = new TestTask();
+ // legacy plugins may assign the protected fields directly
+ task.needAlert = true;
+ TaskAlertInfo alertInfo = new TaskAlertInfo();
+ alertInfo.setTitle("legacy alert");
+ task.taskAlertInfo = alertInfo;
+
+ Assertions.assertTrue(task.getNeedAlert());
+ Assertions.assertSame(alertInfo, task.getTaskAlertInfo());
+ }
+
+ @Test
+ public void testSettersWriteBothFieldAndContext() {
+ TaskExecutionContext ctx = new TaskExecutionContext();
+ AbstractTask task = new TestTask(ctx);
+ task.setNeedAlert(true);
+ TaskAlertInfo alertInfo = new TaskAlertInfo();
+ alertInfo.setTitle("setter alert");
+ task.setTaskAlertInfo(alertInfo);
+
+ Assertions.assertTrue(ctx.isNeedAlert());
+ Assertions.assertSame(alertInfo, ctx.getTaskAlertInfo());
+ Assertions.assertTrue(task.getNeedAlert());
+ Assertions.assertSame(alertInfo, task.getTaskAlertInfo());
+ }
+
+ @Test
+ public void testGettersFallBackToContextWhenFieldNotSet() {
+ TaskExecutionContext ctx = new TaskExecutionContext();
+ AbstractTask task = new TestTask(ctx);
+ ctx.setNeedAlert(true);
+ TaskAlertInfo alertInfo = new TaskAlertInfo();
+ alertInfo.setTitle("context alert");
+ ctx.setTaskAlertInfo(alertInfo);
+
+ Assertions.assertTrue(task.getNeedAlert());
+ Assertions.assertSame(alertInfo, task.getTaskAlertInfo());
+ }
+
+ @Test
+ public void testGettersReturnDefaultsWhenNothingSet() {
+ AbstractTask task = new TestTask();
+
+ Assertions.assertFalse(task.getNeedAlert());
+ Assertions.assertNull(task.getTaskAlertInfo());
+ }
+
private static final class TestTask extends AbstractTask {
private TestTask() {
super(new TaskExecutionContext());
}
+ private TestTask(TaskExecutionContext taskExecutionContext) {
+ super(taskExecutionContext);
+ }
+
@Override
public void handle(TaskCallBack taskCallBack) {
}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java
index 113dfbcd9e..12c0cce323 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java
@@ -17,8 +17,11 @@
package org.apache.dolphinscheduler.plugin.task.sql;
+import org.apache.dolphinscheduler.common.constants.Constants;
+import org.apache.dolphinscheduler.common.enums.AlertType;
import org.apache.dolphinscheduler.common.utils.DateUtils;
import org.apache.dolphinscheduler.common.utils.JSONUtils;
+import org.apache.dolphinscheduler.common.utils.PropertyUtils;
import
org.apache.dolphinscheduler.plugin.datasource.api.plugin.DataSourceClientProvider;
import
org.apache.dolphinscheduler.plugin.datasource.api.plugin.DataSourcePluginManager;
import org.apache.dolphinscheduler.plugin.datasource.api.utils.DataSourceUtils;
@@ -56,6 +59,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -66,6 +70,7 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -90,6 +95,34 @@ public class SqlTask extends AbstractTask {
*/
private static final int QUERY_LIMIT = 10000;
+ /**
+ * default max rows kept in the task-result alert content; rows beyond
this are replaced by an omission marker,
+ * overridable via the sql.task.alert.max.rows property in
common.properties
+ */
+ private static final int DEFAULT_ALERT_MAX_ROWS = 30;
+
+ /**
+ * default max length of a single string field value kept in the alert
content; longer values are truncated
+ * with an ellipsis, overridable via the
sql.task.alert.max.row.value.length property in common.properties
+ */
+ private static final int DEFAULT_ALERT_MAX_ROW_VALUE_LENGTH = 100;
+
+ /**
+ * default max serialized UTF-8 bytes of the alert content, kept below the
65535-byte capacity
+ * of the MySQL TEXT column that stores t_ds_alert.content, overridable
via the
+ * sql.task.alert.max.content.bytes property in common.properties
+ */
+ private static final int DEFAULT_ALERT_MAX_CONTENT_BYTES = 60000;
+
+ private final int alertMaxRows =
+ PropertyUtils.getInt(Constants.SQL_TASK_ALERT_MAX_ROWS,
DEFAULT_ALERT_MAX_ROWS);
+
+ private final int alertMaxRowValueLength =
+
PropertyUtils.getInt(Constants.SQL_TASK_ALERT_MAX_ROW_VALUE_LENGTH,
DEFAULT_ALERT_MAX_ROW_VALUE_LENGTH);
+
+ private final int alertMaxContentBytes =
+ PropertyUtils.getInt(Constants.SQL_TASK_ALERT_MAX_CONTENT_BYTES,
DEFAULT_ALERT_MAX_CONTENT_BYTES);
+
private final SQLTaskExecutionContext sqlTaskExecutionContext;
private final DbType dbType;
@@ -287,13 +320,18 @@ public class SqlTask extends AbstractTask {
}
}
- String result = resultJSONArray.isEmpty() ?
JSONUtils.toJsonString(generateEmptyRow(resultSet))
- : JSONUtils.toJsonString(resultJSONArray);
+ String result;
+ ArrayNode alertArray = resultJSONArray;
+ if (resultJSONArray.isEmpty()) {
+ ArrayNode emptyRow = generateEmptyRow(resultSet);
+ result = JSONUtils.toJsonString(emptyRow);
+ alertArray = emptyRow;
+ } else {
+ result = JSONUtils.toJsonString(resultJSONArray);
+ }
if (Boolean.TRUE.equals(sqlParameters.getSendEmail())) {
- sendAttachment(sqlParameters.getGroupId(),
StringUtils.isNotEmpty(sqlParameters.getTitle())
- ? sqlParameters.getTitle()
- : taskExecutionContext.getTaskName() + " query result
sets", result);
+ prepareTaskResultAlert(alertArray);
}
log.debug("execute sql result : {}", result);
return result;
@@ -320,18 +358,99 @@ public class SqlTask extends AbstractTask {
}
/**
- * send alert as an attachment
- *
- * @param title title
- * @param content content
+ * Prepare task result alert info with the complete query result as
content.
*/
- private void sendAttachment(int groupId, String title, String content) {
- setNeedAlert(Boolean.TRUE);
+ private void prepareTaskResultAlert(ArrayNode resultJSONArray) {
TaskAlertInfo taskAlertInfo = new TaskAlertInfo();
- taskAlertInfo.setAlertGroupId(groupId);
- taskAlertInfo.setContent(content);
- taskAlertInfo.setTitle(title);
- setTaskAlertInfo(taskAlertInfo);
+ taskAlertInfo.setAlertGroupId(sqlParameters.getGroupId());
+ taskAlertInfo.setTitle(StringUtils.isNotEmpty(sqlParameters.getTitle())
+ ? sqlParameters.getTitle()
+ : taskExecutionContext.getTaskName() + " query result sets");
+ taskAlertInfo.setContent(limitAlertContent(resultJSONArray));
+ taskAlertInfo.setAlertType(AlertType.TASK_RESULT);
+
+ taskExecutionContext.setNeedAlert(true);
+ taskExecutionContext.setTaskAlertInfo(taskAlertInfo);
+ log.debug("Prepare task result alert: title={}, alertGroupId={},
alertType={}, totalRows={}",
+ taskAlertInfo.getTitle(), taskAlertInfo.getAlertGroupId(),
taskAlertInfo.getAlertType(),
+ resultJSONArray.size());
+ }
+
+ /**
+ * Build a size-limited alert content that stays a valid JSON array.
+ * At most the configured max rows are kept; string field values longer
than the configured
+ * max value length are truncated with an ellipsis. The serialized content
is bounded by
+ * the configured max UTF-8 bytes: rows are dropped from the tail until it
fits; a row that
+ * alone exceeds the budget is replaced by the omission marker.
+ */
+ private String limitAlertContent(ArrayNode resultJSONArray) {
+ int totalRows = resultJSONArray.size();
+ ArrayNode limited = JSONUtils.createArrayNode();
+ int rowsToKeep = Math.min(totalRows, alertMaxRows);
+ for (int i = 0; i < rowsToKeep; i++) {
+ limited.add(limitRowValues(resultJSONArray.get(i)));
+ }
+
+ int omittedRows = totalRows - rowsToKeep;
+ while (true) {
+ if (omittedRows > 0) {
+ limited.add(createOmissionMarker(omittedRows));
+ }
+ String content = JSONUtils.toJsonString(limited);
+ if (content.getBytes(StandardCharsets.UTF_8).length <=
alertMaxContentBytes) {
+ return content;
+ }
+ if (omittedRows > 0) {
+ limited.remove(limited.size() - 1);
+ }
+ if (limited.isEmpty()) {
+ // Even a lone marker exceeds the budget (pathological
budget); return the minimal notice.
+ return
JSONUtils.toJsonString(createOmissionMarkerArray(totalRows));
+ }
+ limited.remove(limited.size() - 1);
+ omittedRows++;
+ }
+ }
+
+ private ObjectNode createOmissionMarker(int omittedRows) {
+ ObjectNode omitted = JSONUtils.createObjectNode();
+ omitted.put("...", omittedRows + " more rows omitted");
+ return omitted;
+ }
+
+ private ArrayNode createOmissionMarkerArray(int totalRows) {
+ ArrayNode markerArray = JSONUtils.createArrayNode();
+ markerArray.add(createOmissionMarker(totalRows));
+ return markerArray;
+ }
+
+ private JsonNode limitRowValues(JsonNode row) {
+ if (!row.isObject()) {
+ return row;
+ }
+ ObjectNode limitedRow = JSONUtils.createObjectNode();
+ Iterator<Map.Entry<String, JsonNode>> fields = row.fields();
+ while (fields.hasNext()) {
+ Map.Entry<String, JsonNode> entry = fields.next();
+ JsonNode value = entry.getValue();
+ if (value.isTextual() && value.textValue().length() >
alertMaxRowValueLength) {
+ limitedRow.put(entry.getKey(),
+ truncateAtCodePointBoundary(value.textValue(),
alertMaxRowValueLength) + "...");
+ } else {
+ limitedRow.set(entry.getKey(), value);
+ }
+ }
+ return limitedRow;
+ }
+
+ /**
+ * Cut a string to at most maxChars characters without splitting a
surrogate pair.
+ */
+ private String truncateAtCodePointBoundary(String text, int maxChars) {
+ if (text.codePointCount(0, text.length()) <= maxChars) {
+ return text;
+ }
+ return text.substring(0, text.offsetByCodePoints(0, maxChars));
}
private String executeQuery(Connection connection, SqlBinds sqlBinds,
String handlerType) throws Exception {
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java
index 92f0fa0702..7dc36bc9e2 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java
@@ -20,8 +20,10 @@ package org.apache.dolphinscheduler.plugin.task.sql;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import org.apache.dolphinscheduler.common.enums.AlertType;
import org.apache.dolphinscheduler.common.utils.DateUtils;
import org.apache.dolphinscheduler.common.utils.JSONUtils;
+import org.apache.dolphinscheduler.common.utils.PropertyUtils;
import org.apache.dolphinscheduler.plugin.task.api.TaskConstants;
import org.apache.dolphinscheduler.plugin.task.api.TaskException;
import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext;
@@ -29,6 +31,7 @@ import
org.apache.dolphinscheduler.plugin.task.api.enums.DataType;
import org.apache.dolphinscheduler.plugin.task.api.enums.Direct;
import org.apache.dolphinscheduler.plugin.task.api.enums.ResourceType;
import org.apache.dolphinscheduler.plugin.task.api.model.Property;
+import org.apache.dolphinscheduler.plugin.task.api.model.TaskAlertInfo;
import org.apache.dolphinscheduler.plugin.task.api.parameters.SqlParameters;
import
org.apache.dolphinscheduler.plugin.task.api.parameters.resource.DataSourceParameters;
import
org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper;
@@ -36,6 +39,7 @@ import
org.apache.dolphinscheduler.plugin.task.api.resource.ResourceContext;
import org.apache.dolphinscheduler.plugin.task.api.utils.ParameterUtils;
import org.apache.dolphinscheduler.spi.enums.DbType;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
@@ -50,7 +54,10 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
@@ -489,4 +496,390 @@ class SqlTaskTest {
return resourceParametersHelper;
}
+ private TaskExecutionContext createSqlTaskWithAlert(ArrayNode resultArray,
int displayRows, String title,
+ int groupId) {
+ SqlParameters sqlParameters = new SqlParameters();
+ sqlParameters.setType("HIVE");
+ sqlParameters.setDatasource(1);
+ sqlParameters.setSql("select 1");
+ sqlParameters.setSendEmail(true);
+ sqlParameters.setDisplayRows(displayRows);
+ sqlParameters.setGroupId(groupId);
+ if (title != null) {
+ sqlParameters.setTitle(title);
+ }
+
+ TaskExecutionContext ctx = new TaskExecutionContext();
+ ctx.setTaskParams(JSONUtils.toJsonString(sqlParameters));
+ ctx.setTaskName("test_sql_task");
+
ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE));
+
+ SqlTask task = new SqlTask(ctx);
+ try {
+ Method method =
SqlTask.class.getDeclaredMethod("prepareTaskResultAlert", ArrayNode.class);
+ method.setAccessible(true);
+ method.invoke(task, resultArray);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return ctx;
+ }
+
+ @Test
+ void testPrepareTaskResultAlertSetsNeedAlertAndTaskAlertInfo() {
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+
resultArray.add(JSONUtils.parseObject("{\"id\":\"1\",\"name\":\"alice\"}"));
+
resultArray.add(JSONUtils.parseObject("{\"id\":\"2\",\"name\":\"bob\"}"));
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10, "My
Alert Title", 5);
+
+ Assertions.assertTrue(ctx.isNeedAlert());
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ Assertions.assertNotNull(alertInfo);
+ Assertions.assertEquals(5, alertInfo.getAlertGroupId());
+ Assertions.assertEquals("My Alert Title", alertInfo.getTitle());
+ Assertions.assertEquals(AlertType.TASK_RESULT,
alertInfo.getAlertType());
+ Assertions.assertNotNull(alertInfo.getContent());
+
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ Assertions.assertEquals(2, contentArray.size());
+ }
+
+ @Test
+ void testPrepareTaskResultAlertKeepsFullContentRegardlessOfDisplayRows()
throws Exception {
+ int totalRows = readStaticIntConstant("DEFAULT_ALERT_MAX_ROWS") - 1;
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ for (int i = 0; i < totalRows; i++) {
+ resultArray.add(JSONUtils.parseObject("{\"id\":\"" + i + "\"}"));
+ }
+
+ // displayRows is only a log-display setting, the alert content must
keep all rows
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 3,
null, 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ Assertions.assertNotNull(alertInfo);
+
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ Assertions.assertEquals(totalRows, contentArray.size());
+ Assertions.assertEquals("0", contentArray.get(0).get("id").asText());
+ Assertions.assertEquals(String.valueOf(totalRows - 1),
contentArray.get(totalRows - 1).get("id").asText());
+ }
+
+ @Test
+ void testPrepareTaskResultAlertKeepsFullContentWhenDisplayRowsUnset()
throws Exception {
+ int totalRows = readStaticIntConstant("DEFAULT_ALERT_MAX_ROWS") - 1;
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ for (int i = 0; i < totalRows; i++) {
+ resultArray.add(JSONUtils.parseObject("{\"id\":\"" + i + "\"}"));
+ }
+
+ // displayRows = 0 means unset, the alert content still keeps all rows
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 0,
"default rows test", 2);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ Assertions.assertNotNull(alertInfo);
+
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ Assertions.assertEquals(totalRows, contentArray.size());
+ }
+
+ @Test
+ void testPrepareTaskResultAlertUsesCustomTitle() {
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ resultArray.add(JSONUtils.parseObject("{\"id\":\"1\"}"));
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10,
"Custom Title", 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ Assertions.assertNotNull(alertInfo);
+ Assertions.assertEquals("Custom Title", alertInfo.getTitle());
+ }
+
+ @Test
+ void testPrepareTaskResultAlertUsesDefaultTitleWhenEmpty() {
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ resultArray.add(JSONUtils.parseObject("{\"id\":\"1\"}"));
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10,
null, 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ Assertions.assertNotNull(alertInfo);
+ Assertions.assertEquals("test_sql_task query result sets",
alertInfo.getTitle());
+ }
+
+ @Test
+ void testPrepareTaskResultAlertTruncatesRowsBeyondMax() throws Exception {
+ int maxRows = readStaticIntConstant("DEFAULT_ALERT_MAX_ROWS");
+ int totalRows = maxRows + 10;
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ for (int i = 0; i < totalRows; i++) {
+ resultArray.add(JSONUtils.parseObject("{\"id\":\"" + i + "\"}"));
+ }
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10,
"truncate rows test", 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ Assertions.assertEquals(maxRows + 1, contentArray.size());
+ Assertions.assertEquals("0", contentArray.get(0).get("id").asText());
+ JsonNode last = contentArray.get(contentArray.size() - 1);
+ Assertions.assertTrue(last.has("..."));
+ Assertions.assertEquals((totalRows - maxRows) + " more rows omitted",
last.get("...").asText());
+ }
+
+ @Test
+ void testPrepareTaskResultAlertTruncatesLongFieldValue() throws Exception {
+ int maxLen =
readStaticIntConstant("DEFAULT_ALERT_MAX_ROW_VALUE_LENGTH");
+ StringBuilder longValue = new StringBuilder();
+ for (int i = 0; i < maxLen + 50; i++) {
+ longValue.append("x");
+ }
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ resultArray.add(JSONUtils.parseObject("{\"id\":\"1\",\"payload\":\"" +
longValue + "\"}"));
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10,
"truncate field test", 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ Assertions.assertEquals(1, contentArray.size());
+ String payload = contentArray.get(0).get("payload").asText();
+ Assertions.assertEquals(maxLen + 3, payload.length());
+ Assertions.assertTrue(payload.endsWith("..."));
+ }
+
+ @Test
+ void testPrepareTaskResultAlertBoundsTotalBytesWithWideRows() throws
Exception {
+ int maxRows = readStaticIntConstant("DEFAULT_ALERT_MAX_ROWS");
+ int maxLen =
readStaticIntConstant("DEFAULT_ALERT_MAX_ROW_VALUE_LENGTH");
+ int budget = readStaticIntConstant("DEFAULT_ALERT_MAX_CONTENT_BYTES");
+
+ // Wide rows: enough columns that keeping all rows overflows the byte
budget
+ // even though every single value stays under the per-value character
limit.
+ int columns = budget / (maxRows * (maxLen - 1)) + 5;
+ int totalRows = maxRows;
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ for (int i = 0; i < totalRows; i++) {
+ ObjectNode row = JSONUtils.createObjectNode();
+ for (int c = 0; c < columns; c++) {
+ row.put("col_" + c, repeatChar('v', maxLen - 1));
+ }
+ resultArray.add(row);
+ }
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10,
"wide rows test", 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ int contentBytes =
alertInfo.getContent().getBytes(StandardCharsets.UTF_8).length;
+ Assertions.assertTrue(contentBytes <= budget,
+ "serialized alert content must stay within the byte budget: "
+ contentBytes);
+
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ int keptRows = contentArray.size() - 1;
+ Assertions.assertTrue(keptRows < totalRows, "rows must be dropped from
the tail to fit the budget");
+ Assertions.assertTrue(contentArray.get(0).has("col_0"));
+ JsonNode last = contentArray.get(contentArray.size() - 1);
+ Assertions.assertTrue(last.has("..."));
+ Assertions.assertEquals((totalRows - keptRows) + " more rows omitted",
last.get("...").asText());
+ }
+
+ @Test
+ void testPrepareTaskResultAlertBoundsTotalBytesWithMultibyteText() throws
Exception {
+ int maxRows = readStaticIntConstant("DEFAULT_ALERT_MAX_ROWS");
+ int maxLen =
readStaticIntConstant("DEFAULT_ALERT_MAX_ROW_VALUE_LENGTH");
+ int budget = readStaticIntConstant("DEFAULT_ALERT_MAX_CONTENT_BYTES");
+
+ // Each character is 3 UTF-8 bytes, so rows within the character
limits still
+ // overflow the byte budget and rows must be dropped from the tail.
+ char cjk = '\u4e2d';
+ int columns = budget / (maxRows * 3 * maxLen) + 5;
+ int totalRows = maxRows;
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ for (int i = 0; i < totalRows; i++) {
+ ObjectNode row = JSONUtils.createObjectNode();
+ for (int c = 0; c < columns; c++) {
+ row.put("col_" + c, repeatChar(cjk, maxLen));
+ }
+ resultArray.add(row);
+ }
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10,
"multibyte test", 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ int contentBytes =
alertInfo.getContent().getBytes(StandardCharsets.UTF_8).length;
+ Assertions.assertTrue(contentBytes <= budget,
+ "multibyte alert content must stay within the byte budget: " +
contentBytes);
+
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ int keptRows = contentArray.size() - 1;
+ Assertions.assertTrue(keptRows < totalRows, "rows must be dropped from
the tail to fit the budget");
+ // Kept values are exactly at the character limit, so they must stay
complete.
+ String firstValue = contentArray.get(0).get("col_0").asText();
+ Assertions.assertEquals(repeatChar(cjk, maxLen), firstValue);
+ JsonNode last = contentArray.get(contentArray.size() - 1);
+ Assertions.assertTrue(last.has("..."));
+ Assertions.assertEquals((totalRows - keptRows) + " more rows omitted",
last.get("...").asText());
+ }
+
+ @Test
+ void testPrepareTaskResultAlertReplacesRowExceedingByteBudgetAlone()
throws Exception {
+ int budget = readStaticIntConstant("DEFAULT_ALERT_MAX_CONTENT_BYTES");
+
+ // Nested values are not truncated per-field, so a single row can
exceed the
+ // budget alone; it must be replaced by the omission marker.
+ ObjectNode row = JSONUtils.createObjectNode();
+ row.put("id", "1");
+ ObjectNode payload = JSONUtils.createObjectNode();
+ payload.put("blob", repeatChar('n', budget * 2));
+ row.set("payload", payload);
+
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ resultArray.add(row);
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10,
"huge nested value test", 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ int contentBytes =
alertInfo.getContent().getBytes(StandardCharsets.UTF_8).length;
+ Assertions.assertTrue(contentBytes <= budget);
+
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ Assertions.assertEquals(1, contentArray.size());
+ JsonNode only = contentArray.get(0);
+ Assertions.assertTrue(only.has("..."));
+ Assertions.assertEquals("1 more rows omitted",
only.get("...").asText());
+ }
+
+ private String repeatChar(char c, int count) {
+ StringBuilder builder = new StringBuilder(count);
+ for (int i = 0; i < count; i++) {
+ builder.append(c);
+ }
+ return builder.toString();
+ }
+
+ @Test
+ void testPrepareTaskResultAlertRespectsConfiguredLimits() throws Exception
{
+ int configuredMaxRows = 3;
+ int configuredMaxValueLength = 10;
+ int configuredMaxBytes = 60000;
+ int totalRows = 10;
+
+ ArrayNode resultArray = JSONUtils.createArrayNode();
+ for (int i = 0; i < totalRows; i++) {
+ resultArray.add(JSONUtils.parseObject("{\"id\":\"" + i + "\"}"));
+ }
+
+ try (MockedStatic<PropertyUtils> propertyUtils =
Mockito.mockStatic(PropertyUtils.class)) {
+ propertyUtils.when(() ->
PropertyUtils.getInt("sql.task.alert.max.rows",
30)).thenReturn(configuredMaxRows);
+ propertyUtils.when(() ->
PropertyUtils.getInt("sql.task.alert.max.row.value.length", 100))
+ .thenReturn(configuredMaxValueLength);
+ propertyUtils.when(() ->
PropertyUtils.getInt("sql.task.alert.max.content.bytes", 60000))
+ .thenReturn(configuredMaxBytes);
+
+ TaskExecutionContext ctx = createSqlTaskWithAlert(resultArray, 10,
"configured limits test", 1);
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ ArrayNode contentArray =
JSONUtils.parseArray(alertInfo.getContent());
+ Assertions.assertEquals(configuredMaxRows + 1,
contentArray.size());
+ JsonNode last = contentArray.get(contentArray.size() - 1);
+ Assertions.assertTrue(last.has("..."));
+ Assertions.assertEquals((totalRows - configuredMaxRows) + " more
rows omitted",
+ last.get("...").asText());
+ }
+ }
+
+ private int readStaticIntConstant(String name) throws Exception {
+ Field field = SqlTask.class.getDeclaredField(name);
+ field.setAccessible(true);
+ return field.getInt(null);
+ }
+
+ @Test
+ void testResultProcessSendEmailDisabledDoesNotSetNeedAlert() throws
Exception {
+ // Build a SqlTask with sendEmail = false
+ String taskParams =
"{\"type\":\"HIVE\",\"datasource\":1,\"sql\":\"select 1\",\"sendEmail\":false}";
+ TaskExecutionContext ctx = new TaskExecutionContext();
+ ctx.setTaskParams(taskParams);
+ ctx.setTaskName("no_alert_task");
+
ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE));
+
+ SqlTask task = new SqlTask(ctx);
+
+ ResultSet mockResultSet = mock(ResultSet.class);
+ ResultSetMetaData mockMetaData = mock(ResultSetMetaData.class);
+ when(mockResultSet.getMetaData()).thenReturn(mockMetaData);
+ when(mockMetaData.getColumnCount()).thenReturn(1);
+ when(mockMetaData.getColumnLabel(1)).thenReturn("id");
+ when(mockResultSet.next()).thenReturn(true, false);
+ when(mockResultSet.getObject(1)).thenReturn("1");
+
+ Method resultProcessMethod =
SqlTask.class.getDeclaredMethod("resultProcess", ResultSet.class);
+ resultProcessMethod.setAccessible(true);
+ resultProcessMethod.invoke(task, mockResultSet);
+
+ Assertions.assertFalse(ctx.isNeedAlert());
+ Assertions.assertNull(ctx.getTaskAlertInfo());
+ }
+
+ @Test
+ void testResultProcessEmptyResultSetPreparesAlertWithEmptyRow() throws
Exception {
+ String taskParams =
"{\"type\":\"HIVE\",\"datasource\":1,\"sql\":\"select 1\""
+ +
",\"sendEmail\":true,\"displayRows\":10,\"groupId\":3,\"title\":\"empty
result\"}";
+
+ TaskExecutionContext ctx = new TaskExecutionContext();
+ ctx.setTaskParams(taskParams);
+ ctx.setTaskName("empty_result_task");
+
ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE));
+
+ SqlTask task = new SqlTask(ctx);
+
+ ResultSet mockResultSet = mock(ResultSet.class);
+ ResultSetMetaData mockMetaData = mock(ResultSetMetaData.class);
+ when(mockResultSet.getMetaData()).thenReturn(mockMetaData);
+ when(mockMetaData.getColumnCount()).thenReturn(2);
+ when(mockMetaData.getColumnLabel(1)).thenReturn("id");
+ when(mockMetaData.getColumnLabel(2)).thenReturn("name");
+ when(mockResultSet.next()).thenReturn(false); // empty result set
+
+ Method resultProcessMethod =
SqlTask.class.getDeclaredMethod("resultProcess", ResultSet.class);
+ resultProcessMethod.setAccessible(true);
+ resultProcessMethod.invoke(task, mockResultSet);
+
+ Assertions.assertTrue(ctx.isNeedAlert());
+
+ TaskAlertInfo alertInfo = ctx.getTaskAlertInfo();
+ Assertions.assertNotNull(alertInfo);
+ Assertions.assertEquals(3, alertInfo.getAlertGroupId());
+ Assertions.assertEquals("empty result", alertInfo.getTitle());
+ Assertions.assertEquals(AlertType.TASK_RESULT,
alertInfo.getAlertType());
+
+ ArrayNode contentArray = JSONUtils.parseArray(alertInfo.getContent());
+ Assertions.assertEquals(1, contentArray.size());
+ Assertions.assertEquals("", contentArray.get(0).get("id").asText());
+ Assertions.assertEquals("", contentArray.get(0).get("name").asText());
+ }
+
+ @Test
+ void testTaskExecutionContextCarriesAlertInfoForEventPropagation() {
+ TaskExecutionContext ctx = new TaskExecutionContext();
+ ctx.setNeedAlert(true);
+
+ TaskAlertInfo alertInfo = new TaskAlertInfo();
+ alertInfo.setTitle("test title");
+ alertInfo.setContent("[{\"id\":\"1\"}]");
+ alertInfo.setAlertGroupId(7);
+ alertInfo.setAlertType(AlertType.TASK_RESULT);
+ ctx.setTaskAlertInfo(alertInfo);
+
+ Assertions.assertTrue(ctx.isNeedAlert());
+ Assertions.assertNotNull(ctx.getTaskAlertInfo());
+ Assertions.assertEquals("test title",
ctx.getTaskAlertInfo().getTitle());
+ Assertions.assertEquals(7, ctx.getTaskAlertInfo().getAlertGroupId());
+ Assertions.assertEquals(AlertType.TASK_RESULT,
ctx.getTaskAlertInfo().getAlertType());
+
+ TaskExecutionContext ctx2 = new TaskExecutionContext();
+ Assertions.assertFalse(ctx2.isNeedAlert());
+ Assertions.assertNull(ctx2.getTaskAlertInfo());
+ }
+
}
diff --git
a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/executor/PhysicalTaskExecutor.java
b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/executor/PhysicalTaskExecutor.java
index 7118a099d5..7d589599b2 100644
---
a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/executor/PhysicalTaskExecutor.java
+++
b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/executor/PhysicalTaskExecutor.java
@@ -23,6 +23,7 @@ import org.apache.dolphinscheduler.common.utils.PropertyUtils;
import org.apache.dolphinscheduler.plugin.storage.api.StorageOperator;
import org.apache.dolphinscheduler.plugin.task.api.AbstractTask;
import org.apache.dolphinscheduler.plugin.task.api.TaskCallBack;
+import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
import org.apache.dolphinscheduler.plugin.task.api.log.TaskLogMarkers;
import org.apache.dolphinscheduler.plugin.task.api.model.ApplicationInfo;
import org.apache.dolphinscheduler.plugin.task.api.resource.ResourceContext;
@@ -91,6 +92,13 @@ public class PhysicalTaskExecutor extends
AbstractTaskExecutor {
@Override
protected TaskExecutorState doTrackTaskPluginStatus() {
+ if (physicalTask.getExitStatus() == TaskExecutionStatus.SUCCESS) {
+ // Synchronize the legacy needAlert/taskAlertInfo fields written
directly
+ // by AbstractTask subclasses into the context, so that the success
+ // lifecycle event carries the alert info of third-party plugins.
+ taskExecutionContext.setNeedAlert(physicalTask.getNeedAlert());
+
taskExecutionContext.setTaskAlertInfo(physicalTask.getTaskAlertInfo());
+ }
return
TaskExecutorStateMappings.mapState(physicalTask.getExitStatus());
}