This is an automated email from the ASF dual-hosted git repository.
zihaoxiang 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 8077c2b7de [Feature-18070][Task] Add Amazon EMR Serverless task plugin
(#18069)
8077c2b7de is described below
commit 8077c2b7de969b71be51e3c4fd5c82b473ce8837
Author: HUANG XIAO <[email protected]>
AuthorDate: Fri Apr 10 14:06:14 2026 +0800
[Feature-18070][Task] Add Amazon EMR Serverless task plugin (#18069)
---
.github/workflows/api-test.yml | 2 +
docs/configs/docsdev.js | 8 +
docs/docs/en/guide/task/emr-serverless.md | 144 ++++++++
docs/docs/zh/guide/task/emr-serverless.md | 144 ++++++++
docs/img/tasks/demo/emr_serverless_create.png | Bin 0 -> 290612 bytes
.../test/cases/tasks/EmrServerlessTaskAPITest.java | 225 ++++++++++++
.../docker/task-emr-serverless/docker-compose.yaml | 66 ++++
.../wiremock/mappings/get-job-run-failed.json | 27 ++
.../wiremock/mappings/get-job-run-success.json | 27 ++
.../wiremock/mappings/start-job-run-failed.json | 17 +
.../wiremock/mappings/start-job-run-success.json | 20 ++
.../emrServerlessFailedWorkflow.json | 47 +++
.../emrServerlessSuccessWorkflow.json | 47 +++
.../src/main/resources/task-type-config.yaml | 1 +
dolphinscheduler-bom/pom.xml | 5 +
.../dolphinscheduler-task-all/pom.xml | 6 +
.../dolphinscheduler-task-emr-serverless/pom.xml | 77 +++++
.../emrserverless/EmrServerlessParameters.java | 81 +++++
.../task/emrserverless/EmrServerlessTask.java | 318 +++++++++++++++++
.../emrserverless/EmrServerlessTaskChannel.java | 37 ++
.../EmrServerlessTaskChannelFactory.java | 37 ++
.../emrserverless/EmrServerlessTaskException.java | 31 ++
.../task/emrserverless/EmrServerlessTaskTest.java | 376 +++++++++++++++++++++
.../task/emrserverless/StartJobRunRequest.json | 16 +
dolphinscheduler-task-plugin/pom.xml | 1 +
.../public/images/task-icons/emr_serverless.png | Bin 0 -> 1015 bytes
.../images/task-icons/emr_serverless_hover.png | Bin 0 -> 1280 bytes
dolphinscheduler-ui/src/locales/en_US/project.ts | 12 +
dolphinscheduler-ui/src/locales/zh_CN/project.ts | 9 +
dolphinscheduler-ui/src/store/project/task-type.ts | 4 +
dolphinscheduler-ui/src/store/project/types.ts | 1 +
.../projects/task/components/node/fields/index.ts | 1 +
.../components/node/fields/use-emr-serverless.ts | 82 +++++
.../projects/task/components/node/format-data.ts | 7 +
.../projects/task/components/node/tasks/index.ts | 2 +
.../components/node/tasks/use-emr-serverless.ts | 72 ++++
.../views/projects/task/components/node/types.ts | 3 +
.../src/views/projects/task/constants/task-type.ts | 5 +
38 files changed, 1958 insertions(+)
diff --git a/.github/workflows/api-test.yml b/.github/workflows/api-test.yml
index f39ffa5dea..20e9eb95ab 100644
--- a/.github/workflows/api-test.yml
+++ b/.github/workflows/api-test.yml
@@ -121,6 +121,8 @@ jobs:
class: org.apache.dolphinscheduler.api.test.cases.OidcLoginAPITest
- name: DependentTaskAPITest
class:
org.apache.dolphinscheduler.api.test.cases.tasks.DependentTaskAPITest
+ - name: EmrServerlessTaskAPITest
+ class:
org.apache.dolphinscheduler.api.test.cases.tasks.EmrServerlessTaskAPITest
env:
RECORDING_PATH: /tmp/recording-${{ matrix.case.name }}
steps:
diff --git a/docs/configs/docsdev.js b/docs/configs/docsdev.js
index 12b1c309c0..834e1e87ba 100644
--- a/docs/configs/docsdev.js
+++ b/docs/configs/docsdev.js
@@ -153,6 +153,10 @@ export default {
title: 'Amazon EMR',
link:
'/en-us/docs/dev/user_doc/guide/task/emr.html',
},
+ {
+ title: 'Amazon EMR Serverless',
+ link:
'/en-us/docs/dev/user_doc/guide/task/emr-serverless.html',
+ },
{
title: 'Apache Zeppelin',
link:
'/en-us/docs/dev/user_doc/guide/task/zeppelin.html',
@@ -877,6 +881,10 @@ export default {
title: 'Amazon EMR',
link:
'/zh-cn/docs/dev/user_doc/guide/task/emr.html',
},
+ {
+ title: 'Amazon EMR Serverless',
+ link:
'/zh-cn/docs/dev/user_doc/guide/task/emr-serverless.html',
+ },
{
title: 'Apache Zeppelin',
link:
'/zh-cn/docs/dev/user_doc/guide/task/zeppelin.html',
diff --git a/docs/docs/en/guide/task/emr-serverless.md
b/docs/docs/en/guide/task/emr-serverless.md
new file mode 100644
index 0000000000..b661aac07a
--- /dev/null
+++ b/docs/docs/en/guide/task/emr-serverless.md
@@ -0,0 +1,144 @@
+# Amazon EMR Serverless
+
+## Overview
+
+Amazon EMR Serverless task type, for submitting and monitoring job runs on
[Amazon EMR
Serverless](https://docs.aws.amazon.com/emr/latest/EMR-Serverless-UserGuide/emr-serverless.html)
applications.
+Unlike traditional EMR on EC2, EMR Serverless requires no cluster
infrastructure management and automatically scales compute resources on demand,
suitable for Spark and Hive workloads.
+
+Using [aws-java-sdk](https://aws.amazon.com/cn/sdk-for-java/) in the
background code, to transfer JSON parameters to a
[StartJobRunRequest](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/emrserverless/model/StartJobRunRequest.html)
object
+and submit it to AWS via the [StartJobRun
API](https://docs.aws.amazon.com/emr-serverless/latest/APIReference/API_StartJobRun.html),
then poll job status via the [GetJobRun
API](https://docs.aws.amazon.com/emr-serverless/latest/APIReference/API_GetJobRun.html)
until completion.
+
+## Create Task
+
+- Click `Project Management -> Project Name -> Workflow Definition`, click the
`Create Workflow` button to enter the DAG editing page.
+- Drag `AmazonEMRServerless` task from the toolbar to the artboard to complete
the creation.
+
+## Task Parameters
+
+[//]: # (TODO: use the commented anchor below once our website template
supports this syntax)
+[//]: # (- Please refer to [DolphinScheduler Task Parameters
Appendix](appendix.md#default-task-parameters) `Default Task
Parameters` section for default parameters.)
+
+- Please refer to [DolphinScheduler Task Parameters Appendix](appendix.md)
`Default Task Parameters` section for default parameters.
+
+| **Parameter** |
**Description**
|
+|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Application Id | EMR Serverless application ID (e.g.
`00fkht2eodujab09`), obtainable from the [EMR Serverless
Console](https://console.aws.amazon.com/emr/home#/serverless)
|
+| Execution Role Arn | ARN of the IAM role for job execution (e.g.
`arn:aws:iam::123456789012:role/EMRServerlessRole`), this role needs
permissions to access S3, Glue, and other services
|
+| Job Name | Job name (optional), used to identify the job in
the EMR Serverless console
|
+| StartJobRunRequest JSON | JSON corresponding to the `JobDriver` and
`ConfigurationOverrides` portions of the
[StartJobRunRequest](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/emrserverless/model/StartJobRunRequest.html),
see examples below. **Note**: `ApplicationId` and `ExecutionRoleArn` do not
need to be included in the JSON as they are automatically injected from the
form parameters above |
+
+
+
+## Task Example
+
+### Submit a Spark Job
+
+This example shows how to create an `EMR_SERVERLESS` task node to submit a
Spark job to an EMR Serverless application.
+
+StartJobRunRequest JSON example (Spark):
+
+```json
+{
+ "JobDriver": {
+ "SparkSubmit": {
+ "EntryPoint": "s3://my-bucket/scripts/my-spark-job.jar",
+ "EntryPointArguments": [
+ "s3://my-bucket/input/",
+ "s3://my-bucket/output/"
+ ],
+ "SparkSubmitParameters": "--class com.example.MySparkApp --conf
spark.executor.cores=4 --conf spark.executor.memory=8g --conf
spark.executor.instances=10"
+ }
+ },
+ "ConfigurationOverrides": {
+ "MonitoringConfiguration": {
+ "S3MonitoringConfiguration": {
+ "LogUri": "s3://my-bucket/emr-serverless-logs/"
+ }
+ }
+ }
+}
+```
+
+### Submit a Hive Job
+
+This example shows how to create an `EMR_SERVERLESS` task node to submit a
Hive query job.
+
+StartJobRunRequest JSON example (Hive):
+
+```json
+{
+ "JobDriver": {
+ "HiveSQL": {
+ "Query": "s3://my-bucket/scripts/my-hive-query.sql",
+ "Parameters": "--hiveconf hive.exec.dynamic.partition=true --hiveconf
hive.exec.dynamic.partition.mode=nonstrict"
+ }
+ },
+ "ConfigurationOverrides": {
+ "MonitoringConfiguration": {
+ "S3MonitoringConfiguration": {
+ "LogUri": "s3://my-bucket/emr-serverless-logs/"
+ }
+ },
+ "ApplicationConfiguration": [
+ {
+ "Classification": "hive-site",
+ "Properties": {
+ "hive.metastore.client.factory.class":
"com.amazonaws.glue.catalog.metastore.AWSGlueDataCatalogHiveClientFactory"
+ }
+ }
+ ]
+ }
+}
+```
+
+## AWS Authentication Configuration
+
+The EMR Serverless task reads AWS credentials from the DolphinScheduler
`aws.yaml` configuration file, under the `aws.emr` section at `conf/aws.yaml`.
+
+### Using IAM Role (Recommended)
+
+If the DolphinScheduler Worker node runs on an EC2 instance with an attached
IAM Role:
+
+```yaml
+aws:
+ emr:
+ credentials.provider.type: InstanceProfileCredentialsProvider
+ region: us-east-1
+```
+
+### Using Access Key
+
+If you need to authenticate using AK/SK:
+
+```yaml
+aws:
+ emr:
+ credentials.provider.type: AWSStaticCredentialsProvider
+ access.key.id: your-access-key-id
+ access.key.secret: your-secret-access-key
+ region: us-east-1
+```
+
+> **Note**: The `aws.emr` section configuration is shared by both EMR on EC2
and EMR Serverless task types.
+
+## Job State Transitions
+
+After an EMR Serverless job is submitted, DolphinScheduler polls the job
status every 10 seconds:
+
+```
+SUBMITTED → PENDING → SCHEDULED → RUNNING → SUCCESS
+ → FAILED
+ → CANCELLED
+```
+
+- When a job reaches `SUCCESS` state, the task is marked as successful
+- When a job reaches `FAILED` or `CANCELLED` state, the task is marked as
failed
+- If a DolphinScheduler task is killed, it automatically calls the
[CancelJobRun
API](https://docs.aws.amazon.com/emr-serverless/latest/APIReference/API_CancelJobRun.html)
to cancel the running job
+
+## Notice
+
+- The **Application Id** must correspond to a pre-existing EMR Serverless
application (created via the AWS Console or API) in `STARTED` or `CREATED` state
+- The **Execution Role** requires the following minimum permissions:
`emr-serverless:StartJobRun`, `emr-serverless:GetJobRun`,
`emr-serverless:CancelJobRun`, plus S3, Glue and other data access permissions
required by the job
+- `StartJobRunRequest JSON` should NOT include `ApplicationId` or
`ExecutionRoleArn` fields — they are automatically injected from the form
parameters
+- EMR Serverless task supports failover: when a Worker node fails, a new
Worker can recover tracking of running jobs through `appIds` (the `jobRunId`)
+
diff --git a/docs/docs/zh/guide/task/emr-serverless.md
b/docs/docs/zh/guide/task/emr-serverless.md
new file mode 100644
index 0000000000..92ce54a84a
--- /dev/null
+++ b/docs/docs/zh/guide/task/emr-serverless.md
@@ -0,0 +1,144 @@
+# Amazon EMR Serverless
+
+## 综述
+
+Amazon EMR Serverless 任务类型,用于向 [Amazon EMR
Serverless](https://docs.aws.amazon.com/emr/latest/EMR-Serverless-UserGuide/emr-serverless.html)
应用程序提交并监控作业运行。
+与传统的 EMR on EC2 不同,EMR Serverless 无需管理集群基础设施,按需自动扩缩容计算资源,适用于 Spark 和 Hive 工作负载。
+
+后台使用 [aws-java-sdk](https://aws.amazon.com/cn/sdk-for-java/) 将 JSON 参数转换为
[StartJobRunRequest](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/emrserverless/model/StartJobRunRequest.html)
对象,
+通过 [StartJobRun
API](https://docs.aws.amazon.com/emr-serverless/latest/APIReference/API_StartJobRun.html)
提交到 AWS,并通过 [GetJobRun
API](https://docs.aws.amazon.com/emr-serverless/latest/APIReference/API_GetJobRun.html)
轮询作业状态直到完成。
+
+## 创建任务
+
+- 点击 `项目管理 -> 项目名称 -> 工作流定义`,点击 `创建工作流` 按钮进入 DAG 编辑页面。
+- 从工具栏中拖拽 `AmazonEMRServerless` 任务到画布中完成创建。
+
+## 任务参数
+
+[//]: # (TODO: use the commented anchor below once our website template
supports this syntax)
+[//]: # (-
默认参数说明请参考[DolphinScheduler任务参数附录](appendix.md#默认任务参数)`默认任务参数`一栏。)
+
+- 默认参数说明请参考[DolphinScheduler任务参数附录](appendix.md)`默认任务参数`一栏。
+
+| **任务参数** |
**描述**
|
+|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Application Id | EMR Serverless 应用程序 ID(格式如 `00fkht2eodujab09`),可在
[EMR Serverless 控制台](https://console.aws.amazon.com/emr/home#/serverless) 获取
|
+| Execution Role Arn | 作业执行 IAM 角色的 ARN(格式如
`arn:aws:iam::123456789012:role/EMRServerlessRole`),该角色需要有访问 S3、Glue 等服务的权限
|
+| Job Name | 作业名称(可选),用于在 EMR Serverless 控制台中标识作业
|
+| StartJobRunRequest JSON |
[StartJobRunRequest](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/emrserverless/model/StartJobRunRequest.html)
中 `JobDriver` 和 `ConfigurationOverrides` 部分对应的
JSON,详细定义见下方示例。**注意**:`ApplicationId` 和 `ExecutionRoleArn` 无需在 JSON
中重复填写,系统会自动从上方参数注入 |
+
+
+
+## 任务样例
+
+### 提交 Spark 作业
+
+该样例展示了如何创建 `EMR_SERVERLESS` 任务节点来提交一个 Spark 作业到 EMR Serverless 应用程序。
+
+StartJobRunRequest JSON 参数样例(Spark):
+
+```json
+{
+ "JobDriver": {
+ "SparkSubmit": {
+ "EntryPoint": "s3://my-bucket/scripts/my-spark-job.jar",
+ "EntryPointArguments": [
+ "s3://my-bucket/input/",
+ "s3://my-bucket/output/"
+ ],
+ "SparkSubmitParameters": "--class com.example.MySparkApp --conf
spark.executor.cores=4 --conf spark.executor.memory=8g --conf
spark.executor.instances=10"
+ }
+ },
+ "ConfigurationOverrides": {
+ "MonitoringConfiguration": {
+ "S3MonitoringConfiguration": {
+ "LogUri": "s3://my-bucket/emr-serverless-logs/"
+ }
+ }
+ }
+}
+```
+
+### 提交 Hive 作业
+
+该样例展示了如何创建 `EMR_SERVERLESS` 任务节点来提交一个 Hive 查询作业。
+
+StartJobRunRequest JSON 参数样例(Hive):
+
+```json
+{
+ "JobDriver": {
+ "HiveSQL": {
+ "Query": "s3://my-bucket/scripts/my-hive-query.sql",
+ "Parameters": "--hiveconf hive.exec.dynamic.partition=true --hiveconf
hive.exec.dynamic.partition.mode=nonstrict"
+ }
+ },
+ "ConfigurationOverrides": {
+ "MonitoringConfiguration": {
+ "S3MonitoringConfiguration": {
+ "LogUri": "s3://my-bucket/emr-serverless-logs/"
+ }
+ },
+ "ApplicationConfiguration": [
+ {
+ "Classification": "hive-site",
+ "Properties": {
+ "hive.metastore.client.factory.class":
"com.amazonaws.glue.catalog.metastore.AWSGlueDataCatalogHiveClientFactory"
+ }
+ }
+ ]
+ }
+}
+```
+
+## AWS 认证配置
+
+EMR Serverless 任务通过 DolphinScheduler 的 `aws.yaml` 配置文件读取 AWS 认证信息,配置路径为
`conf/aws.yaml` 中的 `aws.emr` 段。
+
+### 使用 IAM Role(推荐)
+
+如果 DolphinScheduler Worker 节点运行在 EC2 实例上并已绑定 IAM Role,配置如下:
+
+```yaml
+aws:
+ emr:
+ credentials.provider.type: InstanceProfileCredentialsProvider
+ region: us-east-1
+```
+
+### 使用 Access Key
+
+如果需要使用 AK/SK 方式认证:
+
+```yaml
+aws:
+ emr:
+ credentials.provider.type: AWSStaticCredentialsProvider
+ access.key.id: your-access-key-id
+ access.key.secret: your-secret-access-key
+ region: us-east-1
+```
+
+> **注意**:`aws.emr` 段的配置同时被 EMR on EC2 和 EMR Serverless 任务类型共享。
+
+## 作业状态流转
+
+EMR Serverless 作业提交后,DolphinScheduler 会每 10 秒轮询一次作业状态:
+
+```
+SUBMITTED → PENDING → SCHEDULED → RUNNING → SUCCESS
+ → FAILED
+ → CANCELLED
+```
+
+- 作业进入 `SUCCESS` 状态时,任务标记为成功
+- 作业进入 `FAILED` 或 `CANCELLED` 状态时,任务标记为失败
+- 如果 DolphinScheduler 任务被终止,会自动调用 [CancelJobRun
API](https://docs.aws.amazon.com/emr-serverless/latest/APIReference/API_CancelJobRun.html)
取消正在运行的作业
+
+## 注意事项
+
+- **Application Id** 对应的 EMR Serverless 应用程序需要预先在 AWS 控制台或通过 API 创建,并确保处于
`STARTED` 或 `CREATED` 状态
+- **Execution Role**
需要有以下最小权限:`emr-serverless:StartJobRun`、`emr-serverless:GetJobRun`、`emr-serverless:CancelJobRun`,以及作业所需的
S3、Glue 等数据访问权限
+- `StartJobRunRequest JSON` 中无需填写 `ApplicationId` 和 `ExecutionRoleArn`
字段,系统会自动从表单参数注入
+- EMR Serverless 任务支持故障转移(Failover):当 Worker 节点发生故障时,新的 Worker 可以通过 `appIds`(即
`jobRunId`)恢复对正在运行作业的跟踪
+
diff --git a/docs/img/tasks/demo/emr_serverless_create.png
b/docs/img/tasks/demo/emr_serverless_create.png
new file mode 100644
index 0000000000..ba13ba153e
Binary files /dev/null and b/docs/img/tasks/demo/emr_serverless_create.png
differ
diff --git
a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/java/org/apache/dolphinscheduler/api/test/cases/tasks/EmrServerlessTaskAPITest.java
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/java/org/apache/dolphinscheduler/api/test/cases/tasks/EmrServerlessTaskAPITest.java
new file mode 100644
index 0000000000..655f4026d8
--- /dev/null
+++
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/java/org/apache/dolphinscheduler/api/test/cases/tasks/EmrServerlessTaskAPITest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.api.test.cases.tasks;
+
+import org.apache.dolphinscheduler.api.test.core.DolphinScheduler;
+import org.apache.dolphinscheduler.api.test.entity.HttpResponse;
+import org.apache.dolphinscheduler.api.test.entity.LoginResponseData;
+import org.apache.dolphinscheduler.api.test.pages.LoginPage;
+import org.apache.dolphinscheduler.api.test.pages.project.ProjectPage;
+import org.apache.dolphinscheduler.api.test.pages.workflow.ExecutorPage;
+import
org.apache.dolphinscheduler.api.test.pages.workflow.WorkflowDefinitionPage;
+import
org.apache.dolphinscheduler.api.test.pages.workflow.WorkflowInstancePage;
+import org.apache.dolphinscheduler.api.test.utils.JSONUtils;
+import org.apache.dolphinscheduler.common.enums.FailureStrategy;
+import org.apache.dolphinscheduler.common.enums.ReleaseState;
+import org.apache.dolphinscheduler.common.enums.UserType;
+import org.apache.dolphinscheduler.common.enums.WarningType;
+import org.apache.dolphinscheduler.dao.entity.User;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.DisableIfTestFails;
+
+@DolphinScheduler(composeFiles =
"docker/task-emr-serverless/docker-compose.yaml")
+@Slf4j
+@DisableIfTestFails
+public class EmrServerlessTaskAPITest {
+
+ private static final String username = "admin";
+
+ private static final String password = "dolphinscheduler123";
+
+ private static String sessionId;
+
+ private static User loginUser;
+
+ private static ExecutorPage executorPage;
+
+ private static WorkflowDefinitionPage workflowDefinitionPage;
+
+ private static WorkflowInstancePage workflowInstancePage;
+
+ private static ProjectPage projectPage;
+
+ private static long projectCode;
+
+ private static long workflowDefinitionCode;
+
+ private static List<Integer> workflowInstanceIds;
+
+ @BeforeAll
+ public static void setup() {
+ LoginPage loginPage = new LoginPage();
+ HttpResponse loginHttpResponse = loginPage.login(username, password);
+ sessionId =
+ JSONUtils.convertValue(loginHttpResponse.getBody().getData(),
LoginResponseData.class).getSessionId();
+ executorPage = new ExecutorPage(sessionId);
+ workflowDefinitionPage = new WorkflowDefinitionPage(sessionId);
+ workflowInstancePage = new WorkflowInstancePage(sessionId);
+ projectPage = new ProjectPage(sessionId);
+ loginUser = new User();
+ loginUser.setUserName("admin");
+ loginUser.setId(1);
+ loginUser.setUserType(UserType.ADMIN_USER);
+ }
+
+ @AfterAll
+ public static void cleanup() {
+ log.info("success cleanup");
+ }
+
+ @Test
+ @Order(1)
+ public void testEmrServerlessSuccessWorkflowInstance() throws Exception {
+ String workflowDefinitionName = "test_emr_serverless_success_" +
System.currentTimeMillis();
+ // create test project
+ projectPage.createProject(loginUser, "project-test-emr-serverless");
+ HttpResponse queryAllProjectListResponse =
projectPage.queryAllProjectList(loginUser);
+
Assertions.assertTrue(queryAllProjectListResponse.getBody().getSuccess());
+ projectCode = (long) ((LinkedHashMap<String, Object>)
((List<LinkedHashMap>) queryAllProjectListResponse
+ .getBody().getData()).get(0)).get("code");
+
+ // upload test workflow definition json
+ ClassLoader classLoader = getClass().getClassLoader();
+ File file = new File(classLoader
+
.getResource("workflow-json/task-emr-serverless/emrServerlessSuccessWorkflow.json").getFile());
+ HttpResponse createWorkflowDefinitionResponse = workflowDefinitionPage
+ .createWorkflowDefinition(loginUser, projectCode, file,
workflowDefinitionName);
+
Assertions.assertTrue(createWorkflowDefinitionResponse.getBody().getSuccess());
+
+ // get workflow definition code
+ HttpResponse queryAllWorkflowDefinitionByProjectCodeResponse =
+
workflowDefinitionPage.queryAllWorkflowDefinitionByProjectCode(loginUser,
projectCode);
+
Assertions.assertTrue(queryAllWorkflowDefinitionByProjectCodeResponse.getBody().getSuccess());
+
Assertions.assertTrue(queryAllWorkflowDefinitionByProjectCodeResponse.getBody().getData().toString()
+ .contains("test name"));
+ workflowDefinitionCode =
+ (long) ((LinkedHashMap<String, Object>)
((LinkedHashMap<String, Object>) ((List<LinkedHashMap>)
queryAllWorkflowDefinitionByProjectCodeResponse
+
.getBody().getData()).get(0)).get("workflowDefinition")).get("code");
+
+ // release test workflow
+ HttpResponse releaseWorkflowDefinitionResponse =
workflowDefinitionPage.releaseWorkflowDefinition(loginUser,
+ projectCode, workflowDefinitionCode, ReleaseState.ONLINE);
+
Assertions.assertTrue(releaseWorkflowDefinitionResponse.getBody().getSuccess());
+
+ // trigger workflow instance
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd
HH:mm:ss");
+ Date date = new Date();
+ String scheduleTime = String.format("%s,%s", formatter.format(date),
formatter.format(date));
+ log.info("use current time {} as scheduleTime", scheduleTime);
+ HttpResponse startWorkflowInstanceResponse =
executorPage.startWorkflowInstance(loginUser, projectCode,
+ workflowDefinitionCode, scheduleTime, FailureStrategy.END,
WarningType.NONE);
+
Assertions.assertTrue(startWorkflowInstanceResponse.getBody().getSuccess());
+
+ workflowInstanceIds = (List<Integer>)
startWorkflowInstanceResponse.getBody().getData();
+ Assertions.assertFalse(workflowInstanceIds.isEmpty(), "No workflow
instances were created");
+
+ // Wait for workflow instance to complete (up to 120 seconds, polling
every 2 seconds)
+ int workflowInstanceId = workflowInstanceIds.get(0);
+ log.info("Waiting for EMR Serverless success workflow instance: {}",
workflowInstanceId);
+ boolean completed = false;
+ for (int i = 0; i < 60; i++) {
+ Thread.sleep(2000);
+ HttpResponse queryResponse =
workflowInstancePage.queryWorkflowInstanceById(
+ loginUser, projectCode, workflowInstanceId);
+ LinkedHashMap<String, Object> instanceData =
+ (LinkedHashMap<String, Object>)
queryResponse.getBody().getData();
+ String state = (String) instanceData.get("state");
+ log.info("EMR Serverless success workflow instance state: {}",
state);
+ if ("SUCCESS".equals(state)) {
+ completed = true;
+ break;
+ } else if ("FAILURE".equals(state) || "STOP".equals(state)) {
+ Assertions.fail("EMR Serverless workflow instance expected
SUCCESS but got: " + state);
+ }
+ }
+ Assertions.assertTrue(completed, "EMR Serverless workflow instance did
not complete within 120 seconds");
+ }
+
+ @Test
+ @Order(2)
+ public void testEmrServerlessFailedWorkflowInstance() throws Exception {
+ String workflowDefinitionName = "test_emr_serverless_failed_" +
System.currentTimeMillis();
+
+ // upload failed workflow definition json
+ ClassLoader classLoader = getClass().getClassLoader();
+ File file = new File(classLoader
+
.getResource("workflow-json/task-emr-serverless/emrServerlessFailedWorkflow.json").getFile());
+ HttpResponse createWorkflowDefinitionResponse = workflowDefinitionPage
+ .createWorkflowDefinition(loginUser, projectCode, file,
workflowDefinitionName);
+
Assertions.assertTrue(createWorkflowDefinitionResponse.getBody().getSuccess());
+
+ // get workflow definition code
+ HttpResponse queryAllWorkflowDefinitionByProjectCodeResponse =
+
workflowDefinitionPage.queryAllWorkflowDefinitionByProjectCode(loginUser,
projectCode);
+
Assertions.assertTrue(queryAllWorkflowDefinitionByProjectCodeResponse.getBody().getSuccess());
+ long failedWorkflowDefinitionCode =
+ (long) ((LinkedHashMap<String, Object>)
((LinkedHashMap<String, Object>) ((List<LinkedHashMap>)
queryAllWorkflowDefinitionByProjectCodeResponse
+
.getBody().getData()).get(0)).get("workflowDefinition")).get("code");
+
+ // release
+ HttpResponse releaseWorkflowDefinitionResponse =
workflowDefinitionPage.releaseWorkflowDefinition(
+ loginUser, projectCode, failedWorkflowDefinitionCode,
ReleaseState.ONLINE);
+
Assertions.assertTrue(releaseWorkflowDefinitionResponse.getBody().getSuccess());
+
+ // trigger
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd
HH:mm:ss");
+ Date date = new Date();
+ String scheduleTime = String.format("%s,%s", formatter.format(date),
formatter.format(date));
+ HttpResponse startWorkflowInstanceResponse =
executorPage.startWorkflowInstance(loginUser, projectCode,
+ failedWorkflowDefinitionCode, scheduleTime,
FailureStrategy.END, WarningType.NONE);
+
Assertions.assertTrue(startWorkflowInstanceResponse.getBody().getSuccess());
+
+ List<Integer> failedWorkflowInstanceIds =
+ (List<Integer>)
startWorkflowInstanceResponse.getBody().getData();
+ Assertions.assertFalse(failedWorkflowInstanceIds.isEmpty(), "No
workflow instances were created");
+
+ // Wait for workflow instance to complete (up to 120 seconds, polling
every 2 seconds)
+ int failedWorkflowInstanceId = failedWorkflowInstanceIds.get(0);
+ log.info("Waiting for EMR Serverless failed workflow instance: {}",
failedWorkflowInstanceId);
+ boolean completed = false;
+ for (int i = 0; i < 60; i++) {
+ Thread.sleep(2000);
+ HttpResponse queryResponse =
workflowInstancePage.queryWorkflowInstanceById(
+ loginUser, projectCode, failedWorkflowInstanceId);
+ LinkedHashMap<String, Object> instanceData =
+ (LinkedHashMap<String, Object>)
queryResponse.getBody().getData();
+ String state = (String) instanceData.get("state");
+ log.info("EMR Serverless failed workflow instance state: {}",
state);
+ if ("FAILURE".equals(state) || "STOP".equals(state)) {
+ completed = true;
+ break;
+ } else if ("SUCCESS".equals(state)) {
+ Assertions.fail("EMR Serverless workflow instance expected
FAILURE but got SUCCESS");
+ }
+ }
+ Assertions.assertTrue(completed, "EMR Serverless workflow instance did
not complete within 120 seconds");
+ }
+}
diff --git
a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/docker-compose.yaml
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/docker-compose.yaml
new file mode 100644
index 0000000000..dd0a7b8f76
--- /dev/null
+++
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/docker-compose.yaml
@@ -0,0 +1,66 @@
+#
+# 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.
+#
+
+version: "3.8"
+
+services:
+ dolphinscheduler:
+ image: apache/dolphinscheduler-standalone-server:ci
+ environment:
+
MASTER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_CPU_USAGE_PERCENTAGE_THRESHOLDS: 0.95
+ MASTER_SERVER_LOAD_PROTECTION_MAX_JVM_CPU_USAGE_PERCENTAGE_THRESHOLDS:
0.95
+
MASTER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS:
0.95
+ MASTER_SERVER_LOAD_PROTECTION_MAX_DISK_USAGE_PERCENTAGE_THRESHOLDS: 0.95
+
WORKER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_CPU_USAGE_PERCENTAGE_THRESHOLDS: 0.95
+ WORKER_SERVER_LOAD_PROTECTION_MAX_JVM_CPU_USAGE_PERCENTAGE_THRESHOLDS:
0.95
+
WORKER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS:
0.95
+ WORKER_SERVER_LOAD_PROTECTION_MAX_DISK_USAGE_PERCENTAGE_THRESHOLDS: 0.95
+ WORKER_TENANT_CONFIG_AUTO_CREATE_TENANT_ENABLED: 'true'
+ EMR_SERVERLESS_ENDPOINT: http://wiremock:8080
+ AWS_ACCESS_KEY_ID: test
+ AWS_SECRET_ACCESS_KEY: test
+ AWS_DEFAULT_REGION: us-east-1
+ ports:
+ - "12345:12345"
+ networks:
+ - api-test
+ healthcheck:
+ test: [ "CMD", "curl",
"http://localhost:12345/dolphinscheduler/actuator/health" ]
+ interval: 5s
+ timeout: 60s
+ retries: 120
+ depends_on:
+ wiremock:
+ condition: service_healthy
+
+ wiremock:
+ image: wiremock/wiremock:latest
+ ports:
+ - "8080:8080"
+ networks:
+ - api-test
+ volumes:
+ - ./wiremock:/home/wiremock
+ command: --verbose
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
+ interval: 5s
+ timeout: 10s
+ retries: 30
+
+networks:
+ api-test:
diff --git
a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/get-job-run-failed.json
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/get-job-run-failed.json
new file mode 100644
index 0000000000..f6b8d460bc
--- /dev/null
+++
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/get-job-run-failed.json
@@ -0,0 +1,27 @@
+{
+ "request": {
+ "method": "GET",
+ "urlPattern": "/applications/.*/jobruns/test-job-run-id-failed"
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "jsonBody": {
+ "jobRun": {
+ "applicationId": "test-app-id-456",
+ "jobRunId": "test-job-run-id-failed",
+ "name": "test name failed",
+ "arn":
"arn:aws:emr-serverless:us-east-1:123456789012:/applications/test-app-id-456/jobruns/test-job-run-id-failed",
+ "createdBy":
"arn:aws:iam::123456789012:role/EMRServerlessExecutionRole",
+ "createdAt": "2024-01-01T00:00:00Z",
+ "updatedAt": "2024-01-01T00:01:00Z",
+ "executionRole":
"arn:aws:iam::123456789012:role/EMRServerlessExecutionRole",
+ "state": "FAILED",
+ "stateDetails": "Job run failed",
+ "releaseLabel": "emr-7.12.0"
+ }
+ }
+ }
+}
diff --git
a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/get-job-run-success.json
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/get-job-run-success.json
new file mode 100644
index 0000000000..37b89432b8
--- /dev/null
+++
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/get-job-run-success.json
@@ -0,0 +1,27 @@
+{
+ "request": {
+ "method": "GET",
+ "urlPattern": "/applications/.*/jobruns/test-job-run-id-success"
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "jsonBody": {
+ "jobRun": {
+ "applicationId": "test-app-id-123",
+ "jobRunId": "test-job-run-id-success",
+ "name": "test name",
+ "arn":
"arn:aws:emr-serverless:us-east-1:123456789012:/applications/test-app-id-123/jobruns/test-job-run-id-success",
+ "createdBy":
"arn:aws:iam::123456789012:role/EMRServerlessExecutionRole",
+ "createdAt": "2024-01-01T00:00:00Z",
+ "updatedAt": "2024-01-01T00:01:00Z",
+ "executionRole":
"arn:aws:iam::123456789012:role/EMRServerlessExecutionRole",
+ "state": "SUCCESS",
+ "stateDetails": "Job run completed successfully",
+ "releaseLabel": "emr-7.12.0"
+ }
+ }
+ }
+}
diff --git
a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/start-job-run-failed.json
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/start-job-run-failed.json
new file mode 100644
index 0000000000..1181e3f9ae
--- /dev/null
+++
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/start-job-run-failed.json
@@ -0,0 +1,17 @@
+{
+ "request": {
+ "method": "POST",
+ "urlPattern": "/applications/test-app-id-456/jobruns"
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "jsonBody": {
+ "applicationId": "test-app-id-456",
+ "jobRunId": "test-job-run-id-failed",
+ "arn":
"arn:aws:emr-serverless:us-east-1:123456789012:/applications/test-app-id-456/jobruns/test-job-run-id-failed"
+ }
+ }
+}
diff --git
a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/start-job-run-success.json
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/start-job-run-success.json
new file mode 100644
index 0000000000..ea980ceb1b
--- /dev/null
+++
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/docker/task-emr-serverless/wiremock/mappings/start-job-run-success.json
@@ -0,0 +1,20 @@
+{
+ "request": {
+ "method": "POST",
+ "urlPattern": "/applications/test-app-id-123/jobruns",
+ "bodyPatterns": [
+ { "contains": "test name" }
+ ]
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "jsonBody": {
+ "applicationId": "test-app-id-123",
+ "jobRunId": "test-job-run-id-success",
+ "arn":
"arn:aws:emr-serverless:us-east-1:123456789012:/applications/test-app-id-123/jobruns/test-job-run-id-success"
+ }
+ }
+}
diff --git
a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/workflow-json/task-emr-serverless/emrServerlessFailedWorkflow.json
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/workflow-json/task-emr-serverless/emrServerlessFailedWorkflow.json
new file mode 100644
index 0000000000..598c64a583
--- /dev/null
+++
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/workflow-json/task-emr-serverless/emrServerlessFailedWorkflow.json
@@ -0,0 +1,47 @@
+{
+ "taskDefinitionJson": [
+ {
+ "code": 152040321402529,
+ "delayTime": "0",
+ "description": "",
+ "environmentCode": -1,
+ "failRetryInterval": "1",
+ "failRetryTimes": "0",
+ "flag": "YES",
+ "name": "emr_serverless_failed_test",
+ "taskParams": {
+ "localParams": [],
+ "resourceList": [],
+ "applicationId": "test-app-id-456",
+ "executionRoleArn":
"arn:aws:iam::123456789012:role/EMRServerlessExecutionRole",
+ "jobName": "test name failed",
+ "startJobRunRequestJson":
"{\"jobDriver\":{\"sparkSubmit\":{\"entryPoint\":\"s3://test-bucket/test.py\",\"sparkSubmitParameters\":\"--conf
spark.executor.memory=1g\"}},\"configurationOverrides\":{}}"
+ },
+ "taskPriority": "MEDIUM",
+ "taskType": "EMR_SERVERLESS",
+ "timeout": 0,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": "",
+ "workerGroup": "default",
+ "cpuQuota": -1,
+ "memoryMax": -1,
+ "taskExecuteType": "BATCH"
+ }
+ ],
+ "taskRelationJson": [
+ {
+ "name": "",
+ "preTaskCode": 0,
+ "preTaskVersion": 0,
+ "postTaskCode": 152040321402529,
+ "postTaskVersion": 1,
+ "conditionType": "NONE",
+ "conditionParams": {}
+ }
+ ],
+ "name": "emrServerlessFailedWorkflow",
+ "executionType": "PARALLEL",
+ "description": "",
+ "globalParams": [],
+ "timeout": 0
+}
diff --git
a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/workflow-json/task-emr-serverless/emrServerlessSuccessWorkflow.json
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/workflow-json/task-emr-serverless/emrServerlessSuccessWorkflow.json
new file mode 100644
index 0000000000..e3bde596b2
--- /dev/null
+++
b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/resources/workflow-json/task-emr-serverless/emrServerlessSuccessWorkflow.json
@@ -0,0 +1,47 @@
+{
+ "taskDefinitionJson": [
+ {
+ "code": 152040321402528,
+ "delayTime": "0",
+ "description": "",
+ "environmentCode": -1,
+ "failRetryInterval": "1",
+ "failRetryTimes": "0",
+ "flag": "YES",
+ "name": "emr_serverless_test",
+ "taskParams": {
+ "localParams": [],
+ "resourceList": [],
+ "applicationId": "test-app-id-123",
+ "executionRoleArn":
"arn:aws:iam::123456789012:role/EMRServerlessExecutionRole",
+ "jobName": "test name",
+ "startJobRunRequestJson":
"{\"jobDriver\":{\"sparkSubmit\":{\"entryPoint\":\"s3://test-bucket/test.py\",\"sparkSubmitParameters\":\"--conf
spark.executor.memory=1g\"}},\"configurationOverrides\":{}}"
+ },
+ "taskPriority": "MEDIUM",
+ "taskType": "EMR_SERVERLESS",
+ "timeout": 0,
+ "timeoutFlag": "CLOSE",
+ "timeoutNotifyStrategy": "",
+ "workerGroup": "default",
+ "cpuQuota": -1,
+ "memoryMax": -1,
+ "taskExecuteType": "BATCH"
+ }
+ ],
+ "taskRelationJson": [
+ {
+ "name": "",
+ "preTaskCode": 0,
+ "preTaskVersion": 0,
+ "postTaskCode": 152040321402528,
+ "postTaskVersion": 1,
+ "conditionType": "NONE",
+ "conditionParams": {}
+ }
+ ],
+ "name": "emrServerlessSuccessWorkflow",
+ "executionType": "PARALLEL",
+ "description": "",
+ "globalParams": [],
+ "timeout": 0
+}
diff --git a/dolphinscheduler-api/src/main/resources/task-type-config.yaml
b/dolphinscheduler-api/src/main/resources/task-type-config.yaml
index 51bbae7e9a..e95d4190fc 100644
--- a/dolphinscheduler-api/src/main/resources/task-type-config.yaml
+++ b/dolphinscheduler-api/src/main/resources/task-type-config.yaml
@@ -33,6 +33,7 @@ task:
- 'REMOTESHELL'
cloud:
- 'EMR'
+ - 'EMR_SERVERLESS'
- 'K8S'
- 'DMS'
- 'DATA_FACTORY'
diff --git a/dolphinscheduler-bom/pom.xml b/dolphinscheduler-bom/pom.xml
index 21a1e7d961..22f5e23a3b 100644
--- a/dolphinscheduler-bom/pom.xml
+++ b/dolphinscheduler-bom/pom.xml
@@ -752,6 +752,11 @@
<artifactId>aws-java-sdk-emr</artifactId>
<version>${aws-sdk.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.amazonaws</groupId>
+ <artifactId>aws-java-sdk-emrserverless</artifactId>
+ <version>${aws-sdk.version}</version>
+ </dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml
b/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml
index 8b616170b3..f3e6f88555 100644
--- a/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml
+++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml
@@ -112,6 +112,12 @@
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.dolphinscheduler</groupId>
+ <artifactId>dolphinscheduler-task-emr-serverless</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-task-zeppelin</artifactId>
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/pom.xml
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/pom.xml
new file mode 100644
index 0000000000..81a98a92cf
--- /dev/null
+++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/pom.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.dolphinscheduler</groupId>
+ <artifactId>dolphinscheduler-task-plugin</artifactId>
+ <version>dev-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>dolphinscheduler-task-emr-serverless</artifactId>
+ <packaging>jar</packaging>
+
+ <properties>
+ <plugin.name>task.emr.serverless</plugin.name>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.dolphinscheduler</groupId>
+ <artifactId>dolphinscheduler-task-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dolphinscheduler</groupId>
+ <artifactId>dolphinscheduler-aws-authentication</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.dolphinscheduler</groupId>
+ <artifactId>dolphinscheduler-common</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.amazonaws</groupId>
+ <artifactId>aws-java-sdk-emrserverless</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <phase>package</phase>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessParameters.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessParameters.java
new file mode 100644
index 0000000000..bda8faf49b
--- /dev/null
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessParameters.java
@@ -0,0 +1,81 @@
+/*
+ * 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.plugin.task.emrserverless;
+
+import org.apache.dolphinscheduler.plugin.task.api.model.ResourceInfo;
+import
org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class EmrServerlessParameters extends AbstractParameters {
+
+ /**
+ * EMR Serverless Application ID (required).
+ * The application must be in STARTED or CREATED state.
+ */
+ private String applicationId;
+
+ /**
+ * IAM execution role ARN for the job run (required).
+ */
+ private String executionRoleArn;
+
+ /**
+ * Optional job name. If empty, defaults to the task name.
+ */
+ private String jobName;
+
+ /**
+ * StartJobRun request JSON defining jobDriver and configurationOverrides.
+ * The applicationId and executionRoleArn fields in this JSON will be
overridden
+ * by the top-level parameters above.
+ *
+ * @see <a
href="https://docs.aws.amazon.com/emr-serverless/latest/APIReference/API_StartJobRun.html">API_StartJobRun</a>
+ */
+ private String startJobRunRequestJson;
+
+ @Override
+ public boolean checkParameters() {
+ return StringUtils.isNotEmpty(applicationId)
+ && StringUtils.isNotEmpty(executionRoleArn)
+ && StringUtils.isNotEmpty(startJobRunRequestJson);
+ }
+
+ @Override
+ public List<ResourceInfo> getResourceFilesList() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public String toString() {
+ return "EmrServerlessParameters{"
+ + "applicationId='" + applicationId + '\''
+ + ", executionRoleArn='" + executionRoleArn + '\''
+ + ", jobName='" + jobName + '\''
+ + ", startJobRunRequestJson='" + startJobRunRequestJson + '\''
+ + '}';
+ }
+}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTask.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTask.java
new file mode 100644
index 0000000000..cf8cba8406
--- /dev/null
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTask.java
@@ -0,0 +1,318 @@
+/*
+ * 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.plugin.task.emrserverless;
+
+import static
com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT;
+import static
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
+import static
com.fasterxml.jackson.databind.DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL;
+import static
com.fasterxml.jackson.databind.MapperFeature.REQUIRE_SETTERS_FOR_GETTERS;
+
+import
org.apache.dolphinscheduler.authentication.aws.AWSCredentialsProviderFactor;
+import org.apache.dolphinscheduler.authentication.aws.AwsConfigurationKeys;
+import org.apache.dolphinscheduler.common.constants.SystemConstants;
+import org.apache.dolphinscheduler.common.utils.JSONUtils;
+import org.apache.dolphinscheduler.common.utils.PropertyUtils;
+import org.apache.dolphinscheduler.plugin.task.api.AbstractRemoteTask;
+import org.apache.dolphinscheduler.plugin.task.api.TaskConstants;
+import org.apache.dolphinscheduler.plugin.task.api.TaskException;
+import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext;
+import
org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters;
+import org.apache.dolphinscheduler.plugin.task.api.utils.ParameterUtils;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import lombok.extern.slf4j.Slf4j;
+
+import com.amazonaws.SdkBaseException;
+import com.amazonaws.auth.AWSCredentialsProvider;
+import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.services.emrserverless.AWSEMRServerless;
+import com.amazonaws.services.emrserverless.AWSEMRServerlessClientBuilder;
+import com.amazonaws.services.emrserverless.model.CancelJobRunRequest;
+import com.amazonaws.services.emrserverless.model.CancelJobRunResult;
+import com.amazonaws.services.emrserverless.model.GetJobRunRequest;
+import com.amazonaws.services.emrserverless.model.GetJobRunResult;
+import com.amazonaws.services.emrserverless.model.JobRun;
+import com.amazonaws.services.emrserverless.model.JobRunState;
+import com.amazonaws.services.emrserverless.model.StartJobRunRequest;
+import com.amazonaws.services.emrserverless.model.StartJobRunResult;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.google.common.collect.Sets;
+
+/**
+ * Amazon EMR Serverless Task.
+ * <p>
+ * Submits a job run to an EMR Serverless application and tracks it until
completion.
+ * Supports Spark and Hive job types.
+ * </p>
+ */
+@Slf4j
+public class EmrServerlessTask extends AbstractRemoteTask {
+
+ /**
+ * EMR Serverless job run states that indicate the job is still in
progress.
+ */
+ private static final HashSet<String> WAITING_STATES = Sets.newHashSet(
+ JobRunState.SUBMITTED.toString(),
+ JobRunState.PENDING.toString(),
+ JobRunState.SCHEDULED.toString(),
+ JobRunState.RUNNING.toString());
+
+ /**
+ * ObjectMapper configured for AWS SDK request/response deserialization.
+ */
+ static final ObjectMapper objectMapper = JsonMapper.builder()
+ .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .configure(ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true)
+ .configure(READ_UNKNOWN_ENUM_VALUES_AS_NULL, true)
+ .configure(REQUIRE_SETTERS_FOR_GETTERS, true)
+ .defaultTimeZone(SystemConstants.DEFAULT_TIME_ZONE)
+ .propertyNamingStrategy(new
PropertyNamingStrategies.UpperCamelCaseStrategy())
+ .build();
+
+ private final TaskExecutionContext taskExecutionContext;
+
+ private EmrServerlessParameters emrServerlessParameters;
+
+ private AWSEMRServerless emrServerlessClient;
+
+ /**
+ * jobRunId returned by StartJobRun or recovered from appIds
+ */
+ private String jobRunId;
+
+ protected EmrServerlessTask(TaskExecutionContext taskExecutionContext) {
+ super(taskExecutionContext);
+ this.taskExecutionContext = taskExecutionContext;
+ }
+
+ @Override
+ public void init() {
+ final String taskParams = taskExecutionContext.getTaskParams();
+ emrServerlessParameters = JSONUtils.parseObject(taskParams,
EmrServerlessParameters.class);
+ log.info("Initialize EMR Serverless task params: {}",
JSONUtils.toPrettyJsonString(taskParams));
+
+ if (emrServerlessParameters == null ||
!emrServerlessParameters.checkParameters()) {
+ throw new EmrServerlessTaskException("EMR Serverless task params
are not valid");
+ }
+
+ emrServerlessClient = createEmrServerlessClient();
+ }
+
+ @Override
+ public void submitApplication() throws TaskException {
+ try {
+ StartJobRunRequest request = buildStartJobRunRequest();
+
+ log.info("Submitting EMR Serverless job run to application: {}",
+ emrServerlessParameters.getApplicationId());
+ StartJobRunResult result =
emrServerlessClient.startJobRun(request);
+
+ jobRunId = result.getJobRunId();
+ // Store jobRunId for failover recovery; applicationId is always
available from parameters
+ setAppIds(jobRunId);
+ log.info("Successfully submitted EMR Serverless job run, jobRunId:
{}", jobRunId);
+
+ } catch (EmrServerlessTaskException | SdkBaseException e) {
+ log.error("EMR Serverless task submit failed", e);
+ throw new TaskException("EMR Serverless task submit failed", e);
+ }
+ }
+
+ @Override
+ public void trackApplicationStatus() throws TaskException {
+ try {
+ // Recover jobRunId from appIds if needed (failover case)
+ if (StringUtils.isEmpty(jobRunId) &&
StringUtils.isNotEmpty(getAppIds())) {
+ jobRunId = getAppIds();
+ log.info("Recovered EMR Serverless jobRunId from appIds: {}",
jobRunId);
+ }
+
+ if (StringUtils.isEmpty(jobRunId)) {
+ throw new EmrServerlessTaskException("jobRunId is empty,
cannot track application status");
+ }
+
+ String currentState = getJobRunState();
+ while (WAITING_STATES.contains(currentState)) {
+ TimeUnit.SECONDS.sleep(10);
+ currentState = getJobRunState();
+ }
+
+ final int exitCode = mapStateToExitCode(currentState);
+ setExitStatusCode(exitCode);
+ log.info("EMR Serverless job run [{}] finished with state: {},
exitCode: {}",
+ jobRunId, currentState, exitCode);
+
+ } catch (EmrServerlessTaskException | SdkBaseException e) {
+ log.error("EMR Serverless task tracking failed", e);
+ setExitStatusCode(TaskConstants.EXIT_CODE_FAILURE);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new TaskException("EMR Serverless task tracking
interrupted", e);
+ }
+ }
+
+ @Override
+ public void cancelApplication() throws TaskException {
+ if (StringUtils.isEmpty(jobRunId)) {
+ log.warn("jobRunId is empty, skip cancel");
+ return;
+ }
+ log.info("Cancelling EMR Serverless job run, applicationId: {},
jobRunId: {}",
+ emrServerlessParameters.getApplicationId(), jobRunId);
+ try {
+ CancelJobRunRequest request = new CancelJobRunRequest()
+
.withApplicationId(emrServerlessParameters.getApplicationId())
+ .withJobRunId(jobRunId);
+ CancelJobRunResult result =
emrServerlessClient.cancelJobRun(request);
+ log.info("Cancel job run result: {}", result);
+ } catch (SdkBaseException e) {
+ throw new TaskException("Failed to cancel EMR Serverless job run",
e);
+ }
+ }
+
+ @Override
+ public List<String> getApplicationIds() throws TaskException {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public AbstractParameters getParameters() {
+ return emrServerlessParameters;
+ }
+
+ /**
+ * Build StartJobRunRequest from parameters and user-provided JSON.
+ */
+ private StartJobRunRequest buildStartJobRunRequest() {
+ String startJobRunRequestJson;
+ try {
+ startJobRunRequestJson =
ParameterUtils.convertParameterPlaceholders(
+ emrServerlessParameters.getStartJobRunRequestJson(),
+
ParameterUtils.convert(taskExecutionContext.getPrepareParamsMap()));
+ } catch (Exception e) {
+ throw new EmrServerlessTaskException("Failed to resolve parameter
placeholders", e);
+ }
+
+ StartJobRunRequest request;
+ try {
+ request = objectMapper.readValue(startJobRunRequestJson,
StartJobRunRequest.class);
+ } catch (JsonProcessingException e) {
+ throw new EmrServerlessTaskException(
+ "Cannot parse StartJobRunRequest from JSON: " +
startJobRunRequestJson, e);
+ }
+
+ // Override applicationId and executionRoleArn from top-level
parameters
+ request.setApplicationId(emrServerlessParameters.getApplicationId());
+
request.setExecutionRoleArn(emrServerlessParameters.getExecutionRoleArn());
+
+ // Set job name if provided
+ if (StringUtils.isNotEmpty(emrServerlessParameters.getJobName())) {
+ request.setName(emrServerlessParameters.getJobName());
+ } else {
+ request.setName(taskExecutionContext.getTaskName());
+ }
+
+ // Set client token for idempotency
+ request.setClientToken(taskExecutionContext.getTaskInstanceId() + "-"
+ System.currentTimeMillis());
+
+ return request;
+ }
+
+ /**
+ * Get the current state of the job run.
+ */
+ private String getJobRunState() {
+ GetJobRunRequest request = new GetJobRunRequest()
+ .withApplicationId(emrServerlessParameters.getApplicationId())
+ .withJobRunId(jobRunId);
+ GetJobRunResult result = emrServerlessClient.getJobRun(request);
+
+ if (result == null || result.getJobRun() == null) {
+ throw new EmrServerlessTaskException("Failed to get job run
status");
+ }
+
+ JobRun jobRun = result.getJobRun();
+ String state = jobRun.getState();
+ log.info("EMR Serverless job run [applicationId:{}, jobRunId:{}]
state: {}",
+ emrServerlessParameters.getApplicationId(), jobRunId, state);
+ return state;
+ }
+
+ /**
+ * Map EMR Serverless job run final state to DolphinScheduler exit code.
+ */
+ private int mapStateToExitCode(String state) {
+ if (state == null) {
+ return TaskConstants.EXIT_CODE_FAILURE;
+ }
+ if (JobRunState.SUCCESS.toString().equals(state)) {
+ return TaskConstants.EXIT_CODE_SUCCESS;
+ } else if (JobRunState.CANCELLED.toString().equals(state)) {
+ return TaskConstants.EXIT_CODE_KILL;
+ } else {
+ return TaskConstants.EXIT_CODE_FAILURE;
+ }
+ }
+
+ /**
+ * Create EMR Serverless client.
+ * Strategy: try aws.emr.* config first, fallback to
DefaultAWSCredentialsProviderChain.
+ */
+ protected AWSEMRServerless createEmrServerlessClient() {
+ Map<String, String> awsProperties =
PropertyUtils.getByPrefix("aws.emr.", "");
+ AWSEMRServerlessClientBuilder builder =
AWSEMRServerlessClientBuilder.standard();
+
+ AWSCredentialsProvider credentialsProvider;
+ try {
+ credentialsProvider =
AWSCredentialsProviderFactor.credentialsProvider(awsProperties);
+ log.info("Using AWS credentials from aws.emr.* configuration");
+ } catch (Exception e) {
+ log.info("No valid aws.emr.* credentials config found, falling
back to DefaultAWSCredentialsProviderChain");
+ credentialsProvider =
DefaultAWSCredentialsProviderChain.getInstance();
+ }
+ builder.withCredentials(credentialsProvider);
+
+ String region = awsProperties.get(AwsConfigurationKeys.AWS_REGION);
+
+ // Support custom endpoint for testing with LocalStack or other AWS
mocks
+ String endpoint = PropertyUtils.getString("emr.serverless.endpoint",
System.getenv("EMR_SERVERLESS_ENDPOINT"));
+ if (StringUtils.isNotBlank(endpoint)) {
+ log.info("Using custom EMR Serverless endpoint: {}", endpoint);
+ if (StringUtils.isEmpty(region)) {
+ region = "us-east-1"; // Default region for LocalStack
+ }
+ builder.withEndpointConfiguration(
+ new AwsClientBuilder.EndpointConfiguration(endpoint,
region));
+ } else if (StringUtils.isNotEmpty(region)) {
+ builder.withRegion(region);
+ }
+
+ return builder.build();
+ }
+}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskChannel.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskChannel.java
new file mode 100644
index 0000000000..a63cea6bbb
--- /dev/null
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskChannel.java
@@ -0,0 +1,37 @@
+/*
+ * 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.plugin.task.emrserverless;
+
+import org.apache.dolphinscheduler.common.utils.JSONUtils;
+import org.apache.dolphinscheduler.plugin.task.api.AbstractTask;
+import org.apache.dolphinscheduler.plugin.task.api.TaskChannel;
+import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext;
+import
org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters;
+
+public class EmrServerlessTaskChannel implements TaskChannel {
+
+ @Override
+ public AbstractTask createTask(TaskExecutionContext taskRequest) {
+ return new EmrServerlessTask(taskRequest);
+ }
+
+ @Override
+ public AbstractParameters parseParameters(String taskParams) {
+ return JSONUtils.parseObject(taskParams,
EmrServerlessParameters.class);
+ }
+}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskChannelFactory.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskChannelFactory.java
new file mode 100644
index 0000000000..74e4b0e7cc
--- /dev/null
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskChannelFactory.java
@@ -0,0 +1,37 @@
+/*
+ * 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.plugin.task.emrserverless;
+
+import org.apache.dolphinscheduler.plugin.task.api.TaskChannel;
+import org.apache.dolphinscheduler.plugin.task.api.TaskChannelFactory;
+
+import com.google.auto.service.AutoService;
+
+@AutoService(TaskChannelFactory.class)
+public class EmrServerlessTaskChannelFactory implements TaskChannelFactory {
+
+ @Override
+ public String getName() {
+ return "EMR_SERVERLESS";
+ }
+
+ @Override
+ public TaskChannel create() {
+ return new EmrServerlessTaskChannel();
+ }
+}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskException.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskException.java
new file mode 100644
index 0000000000..ea46565b69
--- /dev/null
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/main/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.plugin.task.emrserverless;
+
+import org.apache.dolphinscheduler.plugin.task.api.TaskException;
+
+public class EmrServerlessTaskException extends TaskException {
+
+ public EmrServerlessTaskException(String message) {
+ super(message);
+ }
+
+ public EmrServerlessTaskException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/test/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskTest.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/test/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskTest.java
new file mode 100644
index 0000000000..e4402ba0ce
--- /dev/null
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/test/java/org/apache/dolphinscheduler/plugin/task/emrserverless/EmrServerlessTaskTest.java
@@ -0,0 +1,376 @@
+/*
+ * 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.plugin.task.emrserverless;
+
+import static
org.apache.dolphinscheduler.plugin.task.api.TaskConstants.EXIT_CODE_FAILURE;
+import static
org.apache.dolphinscheduler.plugin.task.api.TaskConstants.EXIT_CODE_KILL;
+import static
org.apache.dolphinscheduler.plugin.task.api.TaskConstants.EXIT_CODE_SUCCESS;
+import static org.mockito.ArgumentMatchers.any;
+
+import org.apache.dolphinscheduler.common.utils.JSONUtils;
+import org.apache.dolphinscheduler.plugin.task.api.TaskCallBack;
+import org.apache.dolphinscheduler.plugin.task.api.TaskException;
+import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext;
+import org.apache.dolphinscheduler.plugin.task.api.model.ApplicationInfo;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import com.amazonaws.services.emrserverless.AWSEMRServerless;
+import com.amazonaws.services.emrserverless.model.AWSEMRServerlessException;
+import com.amazonaws.services.emrserverless.model.CancelJobRunResult;
+import com.amazonaws.services.emrserverless.model.GetJobRunResult;
+import com.amazonaws.services.emrserverless.model.JobRun;
+import com.amazonaws.services.emrserverless.model.JobRunState;
+import com.amazonaws.services.emrserverless.model.StartJobRunResult;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class EmrServerlessTaskTest {
+
+ private static final String APPLICATION_ID = "00abcdefgh123456";
+ private static final String JOB_RUN_ID = "00abcdefgh123456-jobrun-001";
+ private static final String EXECUTION_ROLE_ARN =
"arn:aws:iam::123456789012:role/EMRServerlessRole";
+
+ private EmrServerlessTask emrServerlessTask;
+ private AWSEMRServerless emrServerlessClient;
+
+ private final TaskCallBack taskCallBack = new TaskCallBack() {
+
+ @Override
+ public void updateRemoteApplicationInfo(int taskInstanceId,
ApplicationInfo applicationInfo) {
+ // No-op: not needed for unit tests
+ }
+
+ @Override
+ public void updateTaskInstanceInfo(int taskInstanceId) {
+ // No-op: not needed for unit tests
+ }
+ };
+
+ @BeforeEach
+ void before() throws Exception {
+ String taskParams = buildEmrServerlessTaskParameters();
+ TaskExecutionContext taskExecutionContext =
Mockito.mock(TaskExecutionContext.class);
+
Mockito.when(taskExecutionContext.getTaskParams()).thenReturn(taskParams);
+
Mockito.lenient().when(taskExecutionContext.getTaskName()).thenReturn("test-emr-serverless-task");
+
Mockito.lenient().when(taskExecutionContext.getTaskInstanceId()).thenReturn(1);
+
+ emrServerlessTask = Mockito.spy(new
EmrServerlessTask(taskExecutionContext));
+
+ // mock emrServerlessClient
+ emrServerlessClient = Mockito.mock(AWSEMRServerless.class);
+
+ // mock startJobRun
+ StartJobRunResult startJobRunResult =
Mockito.mock(StartJobRunResult.class);
+
Mockito.lenient().when(emrServerlessClient.startJobRun(any())).thenReturn(startJobRunResult);
+
Mockito.lenient().when(startJobRunResult.getJobRunId()).thenReturn(JOB_RUN_ID);
+
+ // inject mock client
+
Mockito.doReturn(emrServerlessClient).when(emrServerlessTask).createEmrServerlessClient();
+
+ emrServerlessTask.init();
+ }
+
+ @Test
+ void testHandleSuccess() {
+ // Job goes: SUBMITTED -> RUNNING -> SUCCESS
+ mockJobRunStates(JobRunState.SUBMITTED.toString(),
JobRunState.RUNNING.toString(),
+ JobRunState.SUCCESS.toString());
+
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_SUCCESS,
emrServerlessTask.getExitStatusCode());
+ }
+
+ @Test
+ void testHandleFailed() {
+ // Job goes: SUBMITTED -> RUNNING -> FAILED
+ mockJobRunStates(JobRunState.SUBMITTED.toString(),
JobRunState.RUNNING.toString(),
+ JobRunState.FAILED.toString());
+
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_FAILURE,
emrServerlessTask.getExitStatusCode());
+ }
+
+ @Test
+ void testHandleCancelled() {
+ // Job goes: SUBMITTED -> RUNNING -> CANCELLED
+ mockJobRunStates(JobRunState.SUBMITTED.toString(),
JobRunState.RUNNING.toString(),
+ JobRunState.CANCELLED.toString());
+
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_KILL,
emrServerlessTask.getExitStatusCode());
+ }
+
+ @Test
+ void testHandleFullLifecycle() {
+ // Job goes through all intermediate states: SUBMITTED -> PENDING ->
SCHEDULED -> RUNNING -> SUCCESS
+ mockJobRunStates(JobRunState.SUBMITTED.toString(),
JobRunState.PENDING.toString(),
+ JobRunState.SCHEDULED.toString(),
JobRunState.RUNNING.toString(), JobRunState.SUCCESS.toString());
+
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_SUCCESS,
emrServerlessTask.getExitStatusCode());
+ }
+
+ @Test
+ void testSubmitError() {
+ Mockito.when(emrServerlessClient.startJobRun(any()))
+ .thenThrow(new AWSEMRServerlessException("Access denied"));
+
+ Assertions.assertThrows(TaskException.class, () -> {
+ emrServerlessTask.handle(taskCallBack);
+ });
+ }
+
+ @Test
+ void testGetJobRunReturnsNull() {
+ // First call for submit (need a valid startJobRun response)
+ StartJobRunResult startResult = Mockito.mock(StartJobRunResult.class);
+
Mockito.when(emrServerlessClient.startJobRun(any())).thenReturn(startResult);
+ Mockito.when(startResult.getJobRunId()).thenReturn(JOB_RUN_ID);
+
+ // getJobRun returns null
+ Mockito.when(emrServerlessClient.getJobRun(any())).thenReturn(null);
+
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_FAILURE,
emrServerlessTask.getExitStatusCode());
+ }
+
+ @Test
+ void testCancelApplication() {
+ // Submit first so we have a jobRunId
+ mockJobRunStates(JobRunState.SUBMITTED.toString(),
JobRunState.RUNNING.toString(),
+ JobRunState.SUCCESS.toString());
+ emrServerlessTask.handle(taskCallBack);
+
+ // Now test cancel
+ CancelJobRunResult cancelResult =
Mockito.mock(CancelJobRunResult.class);
+
Mockito.when(emrServerlessClient.cancelJobRun(any())).thenReturn(cancelResult);
+ emrServerlessTask.cancelApplication();
+
+ Mockito.verify(emrServerlessClient).cancelJobRun(any());
+ }
+
+ @Test
+ void testCancelWithEmptyJobRunId() {
+ // Don't submit, so jobRunId is empty — cancel should be a no-op
+ emrServerlessTask.cancelApplication();
+ Mockito.verify(emrServerlessClient,
Mockito.never()).cancelJobRun(any());
+ }
+
+ @Test
+ void testFailoverRecovery() {
+ // Simulate failover: submit the job first
+ mockJobRunStates(JobRunState.SUBMITTED.toString(),
JobRunState.RUNNING.toString(),
+ JobRunState.SUCCESS.toString());
+ emrServerlessTask.handle(taskCallBack);
+
+ // Now create a new task instance simulating failover
+ String taskParams = buildEmrServerlessTaskParameters();
+ TaskExecutionContext failoverContext =
Mockito.mock(TaskExecutionContext.class);
+ Mockito.when(failoverContext.getTaskParams()).thenReturn(taskParams);
+
Mockito.lenient().when(failoverContext.getTaskName()).thenReturn("test-emr-serverless-failover");
+
Mockito.lenient().when(failoverContext.getTaskInstanceId()).thenReturn(2);
+ // Simulate that appIds was persisted from previous run
+ Mockito.when(failoverContext.getAppIds()).thenReturn(JOB_RUN_ID);
+
+ EmrServerlessTask failoverTask = Mockito.spy(new
EmrServerlessTask(failoverContext));
+
Mockito.doReturn(emrServerlessClient).when(failoverTask).createEmrServerlessClient();
+
+ // Reset getJobRun mock for the recovery path — job already completed
+ mockJobRunStates(JobRunState.SUCCESS.toString());
+
+ failoverTask.init();
+ failoverTask.handle(taskCallBack);
+
+ // Should recover and succeed without calling startJobRun again
+ Assertions.assertEquals(EXIT_CODE_SUCCESS,
failoverTask.getExitStatusCode());
+ }
+
+ @Test
+ void testInit() {
+ String taskParams = buildEmrServerlessTaskParameters();
+ TaskExecutionContext ctx = Mockito.mock(TaskExecutionContext.class);
+ Mockito.when(ctx.getTaskParams()).thenReturn(taskParams);
+ Mockito.lenient().when(ctx.getTaskName()).thenReturn("test-init-task");
+ Mockito.lenient().when(ctx.getTaskInstanceId()).thenReturn(100);
+
+ EmrServerlessTask task = Mockito.spy(new EmrServerlessTask(ctx));
+ AWSEMRServerless mockClient = Mockito.mock(AWSEMRServerless.class);
+ Mockito.doReturn(mockClient).when(task).createEmrServerlessClient();
+
+ task.init();
+
+ // Verify initialization
+ Assertions.assertNotNull(task.getParameters());
+ Assertions.assertTrue(task.getParameters() instanceof
EmrServerlessParameters);
+ EmrServerlessParameters params = (EmrServerlessParameters)
task.getParameters();
+ Assertions.assertEquals(APPLICATION_ID, params.getApplicationId());
+ Assertions.assertEquals(EXECUTION_ROLE_ARN,
params.getExecutionRoleArn());
+ Mockito.verify(task).createEmrServerlessClient();
+ }
+
+ @Test
+ void testParametersCheck() {
+ EmrServerlessParameters params = new EmrServerlessParameters();
+
+ // All empty — should fail
+ Assertions.assertFalse(params.checkParameters());
+
+ // Only applicationId — should fail
+ params.setApplicationId(APPLICATION_ID);
+ Assertions.assertFalse(params.checkParameters());
+
+ // applicationId + executionRoleArn — should fail (no JSON)
+ params.setExecutionRoleArn(EXECUTION_ROLE_ARN);
+ Assertions.assertFalse(params.checkParameters());
+
+ // All three — should pass
+ params.setStartJobRunRequestJson("{}");
+ Assertions.assertTrue(params.checkParameters());
+ }
+
+ @Test
+ void testInvalidJson() {
+ // Build params with invalid JSON
+ EmrServerlessParameters params = new EmrServerlessParameters();
+ params.setApplicationId(APPLICATION_ID);
+ params.setExecutionRoleArn(EXECUTION_ROLE_ARN);
+ params.setStartJobRunRequestJson("{invalid json!!!}");
+
+ TaskExecutionContext ctx = Mockito.mock(TaskExecutionContext.class);
+
Mockito.when(ctx.getTaskParams()).thenReturn(JSONUtils.toJsonString(params));
+ Mockito.lenient().when(ctx.getTaskName()).thenReturn("test-bad-json");
+ Mockito.lenient().when(ctx.getTaskInstanceId()).thenReturn(99);
+
+ EmrServerlessTask badJsonTask = Mockito.spy(new
EmrServerlessTask(ctx));
+
Mockito.doReturn(emrServerlessClient).when(badJsonTask).createEmrServerlessClient();
+
+ badJsonTask.init();
+ Assertions.assertThrows(TaskException.class, () -> {
+ badJsonTask.handle(taskCallBack);
+ });
+ }
+
+ @Test
+ void testHandle_PollingFailure() {
+ // Submit succeeds
+ StartJobRunResult startResult = Mockito.mock(StartJobRunResult.class);
+
Mockito.when(emrServerlessClient.startJobRun(any())).thenReturn(startResult);
+ Mockito.when(startResult.getJobRunId()).thenReturn(JOB_RUN_ID);
+
+ // First poll returns RUNNING, second poll throws exception
+ GetJobRunResult runningResult = Mockito.mock(GetJobRunResult.class);
+ JobRun runningJobRun = Mockito.mock(JobRun.class);
+ Mockito.when(runningResult.getJobRun()).thenReturn(runningJobRun);
+
Mockito.when(runningJobRun.getState()).thenReturn(JobRunState.RUNNING.toString());
+
+ Mockito.when(emrServerlessClient.getJobRun(any()))
+ .thenReturn(runningResult)
+ .thenThrow(new AWSEMRServerlessException("Network error"));
+
+ emrServerlessTask.handle(taskCallBack);
+ // When polling fails, task should set exit code to FAILURE
+ Assertions.assertEquals(EXIT_CODE_FAILURE,
emrServerlessTask.getExitStatusCode());
+ }
+
+ @Test
+ void testMapStateToExitCode() throws Exception {
+ // Test SUCCESS state
+ mockJobRunStates(JobRunState.SUCCESS.toString());
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_SUCCESS,
emrServerlessTask.getExitStatusCode());
+
+ // Re-init for next test
+ before();
+ mockJobRunStates(JobRunState.FAILED.toString());
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_FAILURE,
emrServerlessTask.getExitStatusCode());
+
+ // Re-init for next test
+ before();
+ mockJobRunStates(JobRunState.CANCELLED.toString());
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_KILL,
emrServerlessTask.getExitStatusCode());
+
+ // Re-init for unknown state (should default to FAILURE)
+ before();
+ mockJobRunStates("UNKNOWN_STATE");
+ emrServerlessTask.handle(taskCallBack);
+ Assertions.assertEquals(EXIT_CODE_FAILURE,
emrServerlessTask.getExitStatusCode());
+ }
+
+ @Test
+ void testGetApplicationIds() {
+ Assertions.assertTrue(emrServerlessTask.getApplicationIds().isEmpty());
+ }
+
+ // --- Helper methods ---
+
+ private void mockJobRunStates(String... states) {
+ if (states.length == 0) {
+ return;
+ }
+
+ GetJobRunResult[] results = new GetJobRunResult[states.length];
+ for (int i = 0; i < states.length; i++) {
+ GetJobRunResult result = Mockito.mock(GetJobRunResult.class);
+ JobRun jobRun = Mockito.mock(JobRun.class);
+ Mockito.when(result.getJobRun()).thenReturn(jobRun);
+ Mockito.when(jobRun.getState()).thenReturn(states[i]);
+ results[i] = result;
+ }
+
+ if (results.length == 1) {
+
Mockito.when(emrServerlessClient.getJobRun(any())).thenReturn(results[0]);
+ } else {
+ GetJobRunResult first = results[0];
+ GetJobRunResult[] rest = new GetJobRunResult[results.length - 1];
+ System.arraycopy(results, 1, rest, 0, rest.length);
+
Mockito.when(emrServerlessClient.getJobRun(any())).thenReturn(first, rest);
+ }
+ }
+
+ private String buildEmrServerlessTaskParameters() {
+ EmrServerlessParameters params = new EmrServerlessParameters();
+ params.setApplicationId(APPLICATION_ID);
+ params.setExecutionRoleArn(EXECUTION_ROLE_ARN);
+
+ String startJobRunRequestJson;
+ try (InputStream is =
this.getClass().getResourceAsStream("StartJobRunRequest.json")) {
+ assert is != null;
+ startJobRunRequestJson = IOUtils.toString(is,
StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ params.setStartJobRunRequestJson(startJobRunRequestJson);
+
+ return JSONUtils.toJsonString(params);
+ }
+}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/test/resources/org/apache/dolphinscheduler/plugin/task/emrserverless/StartJobRunRequest.json
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/test/resources/org/apache/dolphinscheduler/plugin/task/emrserverless/StartJobRunRequest.json
new file mode 100644
index 0000000000..36386900a8
--- /dev/null
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr-serverless/src/test/resources/org/apache/dolphinscheduler/plugin/task/emrserverless/StartJobRunRequest.json
@@ -0,0 +1,16 @@
+{
+ "JobDriver": {
+ "SparkSubmit": {
+ "EntryPoint": "s3://my-bucket/scripts/spark-etl.py",
+ "EntryPointArguments": ["--input", "s3://data/raw", "--output",
"s3://data/processed"],
+ "SparkSubmitParameters": "--conf spark.executor.cores=4 --conf
spark.executor.memory=16g --conf spark.executor.instances=10"
+ }
+ },
+ "ConfigurationOverrides": {
+ "MonitoringConfiguration": {
+ "S3MonitoringConfiguration": {
+ "LogUri": "s3://my-bucket/emr-serverless-logs/"
+ }
+ }
+ }
+}
diff --git a/dolphinscheduler-task-plugin/pom.xml
b/dolphinscheduler-task-plugin/pom.xml
index 6f3d727d59..830650f50f 100644
--- a/dolphinscheduler-task-plugin/pom.xml
+++ b/dolphinscheduler-task-plugin/pom.xml
@@ -62,6 +62,7 @@
<module>dolphinscheduler-task-datafactory</module>
<module>dolphinscheduler-task-remoteshell</module>
<module>dolphinscheduler-task-aliyunserverlessspark</module>
+ <module>dolphinscheduler-task-emr-serverless</module>
</modules>
<dependencyManagement>
diff --git a/dolphinscheduler-ui/public/images/task-icons/emr_serverless.png
b/dolphinscheduler-ui/public/images/task-icons/emr_serverless.png
new file mode 100644
index 0000000000..40fca7c961
Binary files /dev/null and
b/dolphinscheduler-ui/public/images/task-icons/emr_serverless.png differ
diff --git
a/dolphinscheduler-ui/public/images/task-icons/emr_serverless_hover.png
b/dolphinscheduler-ui/public/images/task-icons/emr_serverless_hover.png
new file mode 100644
index 0000000000..59acf7a114
Binary files /dev/null and
b/dolphinscheduler-ui/public/images/task-icons/emr_serverless_hover.png differ
diff --git a/dolphinscheduler-ui/src/locales/en_US/project.ts
b/dolphinscheduler-ui/src/locales/en_US/project.ts
index f10e7fbdd0..1e8f23bba5 100644
--- a/dolphinscheduler-ui/src/locales/en_US/project.ts
+++ b/dolphinscheduler-ui/src/locales/en_US/project.ts
@@ -746,6 +746,18 @@ export default {
emr_flow_define_json_tips: 'Please enter the definition of the job flow.',
emr_steps_define_json: 'stepsDefineJson',
emr_steps_define_json_tips: 'Please enter the definition of the emr step.',
+ emr_serverless_application_id: 'Application ID',
+ emr_serverless_application_id_tips:
+ 'Please enter the EMR Serverless Application ID',
+ emr_serverless_execution_role_arn: 'Execution Role ARN',
+ emr_serverless_execution_role_arn_tips:
+ 'Please enter the IAM execution role ARN',
+ emr_serverless_job_name: 'Job Name',
+ emr_serverless_job_name_tips:
+ 'Optional. Defaults to the task name if empty',
+ emr_serverless_start_job_run_json: 'StartJobRun JSON',
+ emr_serverless_start_job_run_json_tips:
+ 'Please enter the StartJobRun request JSON (jobDriver,
configurationOverrides, etc.)',
zeppelin_note_id: 'zeppelinNoteId',
zeppelin_note_id_tips: 'Please enter the note id of your zeppelin note',
zeppelin_paragraph_id: 'zeppelinParagraphId',
diff --git a/dolphinscheduler-ui/src/locales/zh_CN/project.ts
b/dolphinscheduler-ui/src/locales/zh_CN/project.ts
index a3ad17816a..8876969f72 100644
--- a/dolphinscheduler-ui/src/locales/zh_CN/project.ts
+++ b/dolphinscheduler-ui/src/locales/zh_CN/project.ts
@@ -728,6 +728,15 @@ export default {
emr_flow_define_json_tips: '请输入工作流定义',
emr_steps_define_json: 'stepsDefineJson',
emr_steps_define_json_tips: '请输入EMR步骤定义',
+ emr_serverless_application_id: 'Application ID',
+ emr_serverless_application_id_tips: '请输入 EMR Serverless Application ID',
+ emr_serverless_execution_role_arn: '执行角色 ARN',
+ emr_serverless_execution_role_arn_tips: '请输入 IAM 执行角色 ARN',
+ emr_serverless_job_name: '作业名称',
+ emr_serverless_job_name_tips: '可选,为空时默认使用任务名称',
+ emr_serverless_start_job_run_json: 'StartJobRun JSON',
+ emr_serverless_start_job_run_json_tips:
+ '请输入 StartJobRun 请求 JSON(jobDriver、configurationOverrides 等)',
zeppelin_note_id: 'zeppelinNoteId',
zeppelin_note_id_tips: '请输入zeppelin note id',
zeppelin_paragraph_id: 'zeppelinParagraphId',
diff --git a/dolphinscheduler-ui/src/store/project/task-type.ts
b/dolphinscheduler-ui/src/store/project/task-type.ts
index b6de8e6e5a..597340750a 100644
--- a/dolphinscheduler-ui/src/store/project/task-type.ts
+++ b/dolphinscheduler-ui/src/store/project/task-type.ts
@@ -81,6 +81,10 @@ export const TASK_TYPES_MAP = {
alias: 'AmazonEMR',
helperLinkDisable: true
},
+ EMR_SERVERLESS: {
+ alias: 'AmazonEMRServerless',
+ helperLinkDisable: true
+ },
ZEPPELIN: {
alias: 'ZEPPELIN',
helperLinkDisable: true
diff --git a/dolphinscheduler-ui/src/store/project/types.ts
b/dolphinscheduler-ui/src/store/project/types.ts
index ba7773fa5b..a58135b3e7 100644
--- a/dolphinscheduler-ui/src/store/project/types.ts
+++ b/dolphinscheduler-ui/src/store/project/types.ts
@@ -38,6 +38,7 @@ type TaskType =
| 'SWITCH'
| 'SEATUNNEL'
| 'EMR'
+ | 'EMR_SERVERLESS'
| 'ZEPPELIN'
| 'K8S'
| 'JUPYTER'
diff --git
a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts
b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts
index 43c1ed66d1..da4cce0763 100644
---
a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts
+++
b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts
@@ -66,6 +66,7 @@ export { useDataX } from './use-datax'
export { useConditions } from './use-conditions'
export { useDependent } from './use-dependent'
export { useEmr } from './use-emr'
+export { useEmrServerless } from './use-emr-serverless'
export { useZeppelin } from './use-zeppelin'
export { useNamespace } from './use-namespace'
export { useK8s } from './use-k8s'
diff --git
a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-emr-serverless.ts
b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-emr-serverless.ts
new file mode 100644
index 0000000000..ec164275da
--- /dev/null
+++
b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-emr-serverless.ts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { useI18n } from 'vue-i18n'
+import { useCustomParams } from '.'
+import type { IJsonItem } from '../types'
+
+export function useEmrServerless(model: { [field: string]: any }): IJsonItem[]
{
+ const { t } = useI18n()
+
+ return [
+ {
+ type: 'input',
+ field: 'applicationId',
+ span: 24,
+ name: t('project.node.emr_serverless_application_id'),
+ props: {
+ placeholder: t('project.node.emr_serverless_application_id_tips')
+ },
+ validate: {
+ required: true,
+ trigger: ['input', 'blur'],
+ message: t('project.node.emr_serverless_application_id_tips')
+ }
+ },
+ {
+ type: 'input',
+ field: 'executionRoleArn',
+ span: 24,
+ name: t('project.node.emr_serverless_execution_role_arn'),
+ props: {
+ placeholder: t('project.node.emr_serverless_execution_role_arn_tips')
+ },
+ validate: {
+ required: true,
+ trigger: ['input', 'blur'],
+ message: t('project.node.emr_serverless_execution_role_arn_tips')
+ }
+ },
+ {
+ type: 'input',
+ field: 'jobName',
+ span: 24,
+ name: t('project.node.emr_serverless_job_name'),
+ props: {
+ placeholder: t('project.node.emr_serverless_job_name_tips')
+ }
+ },
+ {
+ type: 'editor',
+ field: 'startJobRunRequestJson',
+ span: 24,
+ name: t('project.node.emr_serverless_start_job_run_json'),
+ props: {
+ language: 'json'
+ },
+ validate: {
+ trigger: ['input', 'trigger'],
+ required: true,
+ message: t('project.node.emr_serverless_start_job_run_json_tips')
+ }
+ },
+ ...useCustomParams({
+ model,
+ field: 'localParams',
+ isSimple: true
+ })
+ ]
+}
diff --git
a/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts
b/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts
index 974da0b44c..a7754cb24d 100644
--- a/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts
+++ b/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts
@@ -304,6 +304,13 @@ export function formatParams(data: INodeData): {
taskParams.stepsDefineJson = data.stepsDefineJson
}
+ if (data.taskType === 'EMR_SERVERLESS') {
+ taskParams.applicationId = data.applicationId
+ taskParams.executionRoleArn = data.executionRoleArn
+ taskParams.jobName = data.jobName
+ taskParams.startJobRunRequestJson = data.startJobRunRequestJson
+ }
+
if (data.taskType === 'ZEPPELIN') {
taskParams.noteId = data.noteId
taskParams.paragraphId = data.paragraphId
diff --git
a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts
b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts
index 3db6bb96ea..89fa49e17b 100644
--- a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts
+++ b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts
@@ -33,6 +33,7 @@ import { useConditions } from './use-conditions'
import { useDataX } from './use-datax'
import { useDependent } from './use-dependent'
import { useEmr } from './use-emr'
+import { useEmrServerless } from './use-emr-serverless'
import { useZeppelin } from './use-zeppelin'
import { useK8s } from './use-k8s'
import { useJupyter } from './use-jupyter'
@@ -70,6 +71,7 @@ export default {
DATAX: useDataX,
DEPENDENT: useDependent,
EMR: useEmr,
+ EMR_SERVERLESS: useEmrServerless,
ZEPPELIN: useZeppelin,
K8S: useK8s,
JUPYTER: useJupyter,
diff --git
a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-emr-serverless.ts
b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-emr-serverless.ts
new file mode 100644
index 0000000000..1b34082627
--- /dev/null
+++
b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-emr-serverless.ts
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { reactive } from 'vue'
+import * as Fields from '../fields/index'
+import type { IJsonItem, INodeData } from '../types'
+import { ITaskData } from '../types'
+
+export function useEmrServerless({
+ projectCode,
+ from = 0,
+ readonly,
+ data
+}: {
+ projectCode: number
+ from?: number
+ readonly?: boolean
+ data?: ITaskData
+}) {
+ const model = reactive({
+ name: '',
+ taskType: 'EMR_SERVERLESS',
+ flag: 'YES',
+ description: '',
+ timeoutFlag: false,
+ localParams: [],
+ environmentCode: null,
+ failRetryInterval: 1,
+ failRetryTimes: 0,
+ workerGroup: 'default',
+ delayTime: 0,
+ timeout: 30,
+ applicationId: '',
+ executionRoleArn: '',
+ jobName: '',
+ startJobRunRequestJson: '',
+ timeoutNotifyStrategy: ['WARN']
+ } as INodeData)
+
+ return {
+ json: [
+ Fields.useName(from),
+ ...Fields.useTaskDefinition({ projectCode, from, readonly, data, model
}),
+ Fields.useRunFlag(),
+ Fields.useDescription(),
+ Fields.useTaskPriority(),
+ Fields.useWorkerGroup(projectCode),
+ Fields.useEnvironmentName(model, !data?.id),
+ ...Fields.useTaskGroup(model, projectCode),
+ ...Fields.useFailed(),
+ Fields.useDelayTime(model),
+ ...Fields.useTimeoutAlarm(model),
+ ...Fields.useEmrServerless(model),
+ Fields.usePreTasks()
+ ] as IJsonItem[],
+ model
+ }
+}
diff --git
a/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts
b/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts
index b576ca8f1a..218f318db0 100644
--- a/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts
+++ b/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts
@@ -348,6 +348,9 @@ interface ITaskParams {
ruleInputParameter?: IRuleParameters
jobFlowDefineJson?: string
stepsDefineJson?: string
+ applicationId?: string
+ executionRoleArn?: string
+ startJobRunRequestJson?: string
zeppelinNoteId?: string
zeppelinParagraphId?: string
zeppelinRestEndpoint?: string
diff --git a/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts
b/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts
index ce0db268f4..d0eb36b277 100644
--- a/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts
+++ b/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts
@@ -32,6 +32,7 @@ export type TaskType =
| 'SWITCH'
| 'SEATUNNEL'
| 'EMR'
+ | 'EMR_SERVERLESS'
| 'ZEPPELIN'
| 'K8S'
| 'JUPYTER'
@@ -114,6 +115,10 @@ export const TASK_TYPES_MAP = {
alias: 'AmazonEMR',
helperLinkDisable: true
},
+ EMR_SERVERLESS: {
+ alias: 'AmazonEMRServerless',
+ helperLinkDisable: true
+ },
ZEPPELIN: {
alias: 'ZEPPELIN',
helperLinkDisable: true