This is an automated email from the ASF dual-hosted git repository.
nicholasjiang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-webui.git
The following commit(s) were added to refs/heads/main by this push:
new 6981175 [Feature] Introduce job submission interface (#222)
6981175 is described below
commit 698117546d784de7c79709c8afd4676f10cac54d
Author: s7monk <[email protected]>
AuthorDate: Thu May 16 15:37:16 2024 +0800
[Feature] Introduce job submission interface (#222)
---
.../web/server/controller/JobController.java | 120 ++++++
.../paimon/web/server/data/dto/JobSubmitDTO.java | 40 ++
.../paimon/web/server/data/dto/ResultFetchDTO.java | 36 ++
.../paimon/web/server/data/dto/StopJobDTO.java | 34 ++
.../paimon/web/server/data/model/JobInfo.java | 62 +++
.../web/server/data/result/enums/Status.java | 8 +-
.../paimon/web/server/data/vo/JobStatisticsVO.java | 42 ++
.../paimon/web/server/data/vo/JobStatusVO.java | 36 ++
.../apache/paimon/web/server/data/vo/JobVO.java | 64 +++
.../paimon/web/server/data/vo/ResultDataVO.java | 43 ++
.../apache/paimon/web/server/mapper/JobMapper.java | 28 ++
.../web/server/service/JobExecutorService.java | 40 ++
.../paimon/web/server/service/JobService.java | 96 +++++
.../web/server/service/impl/JobServiceImpl.java | 478 +++++++++++++++++++++
.../paimon/web/server/util/LocalDateTimeUtil.java | 36 ++
.../src/main/resources/i18n/messages.properties | 4 +
.../main/resources/i18n/messages_en_US.properties | 4 +
.../main/resources/i18n/messages_zh_CN.properties | 4 +
.../web/server/constant/StatementsConstant.java | 76 ++++
.../web/server/controller/JobControllerTest.java | 404 +++++++++++++++++
scripts/sql/paimon-mysql.sql | 18 +
21 files changed, 1672 insertions(+), 1 deletion(-)
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/JobController.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/JobController.java
new file mode 100644
index 0000000..a7be95d
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/JobController.java
@@ -0,0 +1,120 @@
+/*
+ * 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.paimon.web.server.controller;
+
+import org.apache.paimon.web.server.data.dto.JobSubmitDTO;
+import org.apache.paimon.web.server.data.dto.ResultFetchDTO;
+import org.apache.paimon.web.server.data.dto.StopJobDTO;
+import org.apache.paimon.web.server.data.model.JobInfo;
+import org.apache.paimon.web.server.data.result.R;
+import org.apache.paimon.web.server.data.result.enums.Status;
+import org.apache.paimon.web.server.data.vo.JobStatisticsVO;
+import org.apache.paimon.web.server.data.vo.JobStatusVO;
+import org.apache.paimon.web.server.data.vo.JobVO;
+import org.apache.paimon.web.server.data.vo.ResultDataVO;
+import org.apache.paimon.web.server.service.JobService;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/** Job submit api controller. */
+@Slf4j
+@RestController
+@RequestMapping("/api/job")
+public class JobController {
+
+ @Autowired private JobService jobService;
+
+ @SaCheckPermission("playground:job:submit")
+ @PostMapping("/submit")
+ public R<JobVO> submit(@RequestBody JobSubmitDTO jobSubmitDTO) {
+ try {
+ return R.succeed(jobService.submitJob(jobSubmitDTO));
+ } catch (Exception e) {
+ log.error("Exception with submitting a job.", e);
+ return R.failed(Status.JOB_SUBMIT_ERROR);
+ }
+ }
+
+ @SaCheckPermission("playground:job:fetch")
+ @PostMapping("/fetch")
+ public R<ResultDataVO> fetchResult(@RequestBody ResultFetchDTO
resultFetchDTO) {
+ try {
+ return R.succeed(jobService.fetchResult(resultFetchDTO));
+ } catch (Exception e) {
+ log.error("Exception with fetching result data.", e);
+ return R.failed(Status.RESULT_FETCH_ERROR);
+ }
+ }
+
+ @SaCheckPermission("playground:job:list")
+ @GetMapping("/list")
+ public R<List<JobVO>> list() {
+ return R.succeed(jobService.listJobs());
+ }
+
+ @SaCheckPermission("playground:job:list")
+ @GetMapping("/list/page")
+ public R<List<JobVO>> listJobsByPage(int current, int size) {
+ return R.succeed(jobService.listJobsByPage(current, size));
+ }
+
+ @SaCheckPermission("playground:job:query")
+ @GetMapping("/status/get/{jobId}")
+ public R<JobStatusVO> getJobStatus(@PathVariable("jobId") String jobId) {
+ JobInfo job = jobService.getJobById(jobId);
+ JobStatusVO jobStatusVO =
+
JobStatusVO.builder().jobId(job.getJobId()).status(job.getStatus()).build();
+ return R.succeed(jobStatusVO);
+ }
+
+ @SaCheckPermission("playground:job:query")
+ @GetMapping("/statistics/get")
+ public R<JobStatisticsVO> getJobStatistics() {
+ return R.succeed(jobService.getJobStatistics());
+ }
+
+ @SaCheckPermission("playground:job:stop")
+ @PostMapping("/stop")
+ public R<Void> stop(@RequestBody StopJobDTO stopJobDTO) {
+ try {
+ jobService.stop(stopJobDTO);
+ return R.succeed();
+ } catch (Exception e) {
+ log.error("Exception with stopping a job.", e);
+ return R.failed(Status.JOB_STOP_ERROR);
+ }
+ }
+
+ @SaCheckPermission("playground:job:refresh")
+ @PostMapping("/refresh")
+ public R<Void> refresh() {
+ jobService.refreshJobStatus("Flink");
+ return R.succeed();
+ }
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/JobSubmitDTO.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/JobSubmitDTO.java
new file mode 100644
index 0000000..a8cce8b
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/JobSubmitDTO.java
@@ -0,0 +1,40 @@
+/*
+ * 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.paimon.web.server.data.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+/** DTO of submitting job. */
+@Data
+public class JobSubmitDTO {
+
+ private String jobName;
+
+ private String taskType;
+
+ private boolean isStreaming;
+
+ private String clusterId;
+
+ private Map<String, String> config;
+
+ private String statements;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/ResultFetchDTO.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/ResultFetchDTO.java
new file mode 100644
index 0000000..1d34e6f
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/ResultFetchDTO.java
@@ -0,0 +1,36 @@
+/*
+ * 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.paimon.web.server.data.dto;
+
+import lombok.Data;
+
+/** DTO of fetching result. */
+@Data
+public class ResultFetchDTO {
+
+ private String submitId;
+
+ private String clusterId;
+
+ private String sessionId;
+
+ private String taskType;
+
+ private Long token;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/StopJobDTO.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/StopJobDTO.java
new file mode 100644
index 0000000..91430df
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/StopJobDTO.java
@@ -0,0 +1,34 @@
+/*
+ * 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.paimon.web.server.data.dto;
+
+import lombok.Data;
+
+/** DTO of stopping job. */
+@Data
+public class StopJobDTO {
+
+ private String clusterId;
+
+ private String jobId;
+
+ private String taskType;
+
+ private boolean withSavepoint;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/JobInfo.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/JobInfo.java
new file mode 100644
index 0000000..ea05d14
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/JobInfo.java
@@ -0,0 +1,62 @@
+/*
+ * 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.paimon.web.server.data.model;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/** Job table model. */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@TableName("job")
+public class JobInfo extends BaseModel {
+
+ private String jobId;
+
+ private String jobName;
+
+ private String type;
+
+ private String executeMode;
+
+ private String clusterId;
+
+ private Integer uid;
+
+ private String config;
+
+ private String statements;
+
+ private String status;
+
+ private LocalDateTime startTime;
+
+ private LocalDateTime endTime;
+
+ private static final long serialVersionUID = 1L;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
index 29baaa1..4ebb130 100644
---
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
@@ -83,7 +83,13 @@ public enum Status {
/** ------------cluster-----------------. */
CLUSTER_NOT_EXIST(10701, "cluster.not.exist"),
- CLUSTER_NAME_ALREADY_EXISTS(10702, "cluster.name.exist");
+ CLUSTER_NAME_ALREADY_EXISTS(10702, "cluster.name.exist"),
+
+ /** ------------job-----------------. */
+ JOB_SUBMIT_ERROR(10701, "job.submit.error"),
+ RESULT_FETCH_ERROR(10702, "result.fetch.error"),
+ JOB_STOP_ERROR(10703, "job.stop.error"),
+ JOB_UPDATE_STATUS_ERROR(10704, "job.update.status.error");
private final int code;
private final String msg;
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobStatisticsVO.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobStatisticsVO.java
new file mode 100644
index 0000000..802acf5
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobStatisticsVO.java
@@ -0,0 +1,42 @@
+/*
+ * 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.paimon.web.server.data.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/** VO of JobStatistics. */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class JobStatisticsVO {
+
+ private Long totalNum;
+
+ private Long runningNum;
+
+ private Long finishedNum;
+
+ private Long canceledNum;
+
+ private Long failedNum;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobStatusVO.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobStatusVO.java
new file mode 100644
index 0000000..e36cf20
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobStatusVO.java
@@ -0,0 +1,36 @@
+/*
+ * 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.paimon.web.server.data.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/** VO of JobStatus. */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class JobStatusVO {
+
+ private String jobId;
+
+ private String status;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobVO.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobVO.java
new file mode 100644
index 0000000..5e22b96
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/JobVO.java
@@ -0,0 +1,64 @@
+/*
+ * 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.paimon.web.server.data.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/** VO of job. */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class JobVO {
+
+ private String submitId;
+
+ private String jobId;
+
+ private String jobName;
+
+ private String type;
+
+ private String executeMode;
+
+ private String clusterId;
+
+ private String sessionId;
+
+ private Integer uid;
+
+ private String status;
+
+ private Boolean shouldFetchResult;
+
+ private List<Map<String, Object>> resultData;
+
+ private Long token;
+
+ private LocalDateTime startTime;
+
+ private LocalDateTime endTime;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/ResultDataVO.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/ResultDataVO.java
new file mode 100644
index 0000000..097d28b
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/ResultDataVO.java
@@ -0,0 +1,43 @@
+/*
+ * 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.paimon.web.server.data.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+/** VO of result data. */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ResultDataVO {
+
+ private List<Map<String, Object>> resultData;
+
+ private Integer columns;
+
+ private Integer rows;
+
+ private Long token;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/JobMapper.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/JobMapper.java
new file mode 100644
index 0000000..2440fab
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/JobMapper.java
@@ -0,0 +1,28 @@
+/*
+ * 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.paimon.web.server.mapper;
+
+import org.apache.paimon.web.server.data.model.JobInfo;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/** Job mapper. */
+@Mapper
+public interface JobMapper extends BaseMapper<JobInfo> {}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/JobExecutorService.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/JobExecutorService.java
new file mode 100644
index 0000000..db60d1f
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/JobExecutorService.java
@@ -0,0 +1,40 @@
+/*
+ * 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.paimon.web.server.service;
+
+import org.apache.paimon.web.engine.flink.common.executor.Executor;
+
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Job executor service. */
+@Service
+public class JobExecutorService {
+
+ private final ConcurrentHashMap<String, Executor> executors = new
ConcurrentHashMap<>();
+
+ public Executor getExecutor(String sessionId) {
+ return executors.get(sessionId);
+ }
+
+ public void addExecutor(String sessionId, Executor executor) {
+ executors.put(sessionId, executor);
+ }
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/JobService.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/JobService.java
new file mode 100644
index 0000000..c7c65f7
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/JobService.java
@@ -0,0 +1,96 @@
+/*
+ * 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.paimon.web.server.service;
+
+import org.apache.paimon.web.server.data.dto.JobSubmitDTO;
+import org.apache.paimon.web.server.data.dto.ResultFetchDTO;
+import org.apache.paimon.web.server.data.dto.StopJobDTO;
+import org.apache.paimon.web.server.data.model.JobInfo;
+import org.apache.paimon.web.server.data.vo.JobStatisticsVO;
+import org.apache.paimon.web.server.data.vo.JobVO;
+import org.apache.paimon.web.server.data.vo.ResultDataVO;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.List;
+
+/** Job Service. */
+public interface JobService extends IService<JobInfo> {
+
+ /**
+ * Submits a new job based on the provided job submission data.
+ *
+ * @param jobSubmitDTO the data transfer object containing job submission
details
+ * @return the job view object after submission
+ */
+ JobVO submitJob(JobSubmitDTO jobSubmitDTO);
+
+ /**
+ * Fetches the result of a job specified by the result fetch data transfer
object.
+ *
+ * @param resultFetchDTO the data transfer object containing job result
fetching details
+ * @return result data view object containing the job results
+ */
+ ResultDataVO fetchResult(ResultFetchDTO resultFetchDTO);
+
+ /**
+ * Lists all jobs.
+ *
+ * @return a list of job view objects
+ */
+ List<JobVO> listJobs();
+
+ /**
+ * Lists jobs in a paginated format.
+ *
+ * @param current the current page number
+ * @param size the number of jobs per page
+ * @return a list of job view objects for the specified page
+ */
+ List<JobVO> listJobsByPage(int current, int size);
+
+ /**
+ * Retrieves detailed information about a job identified by its job ID.
+ *
+ * @param id the unique identifier of the job
+ * @return job information object
+ */
+ JobInfo getJobById(String id);
+
+ /**
+ * Retrieves statistics about jobs.
+ *
+ * @return a job statistics view object containing aggregated job data
+ */
+ JobStatisticsVO getJobStatistics();
+
+ /**
+ * Stops a job as specified by the stop job data transfer object.
+ *
+ * @param stopJobDTO the data transfer object containing job stopping
details
+ */
+ void stop(StopJobDTO stopJobDTO);
+
+ /**
+ * Refreshes the status of a job based on the specified task type.
+ *
+ * @param taskType the type of task for which the job status needs to be
updated
+ */
+ void refreshJobStatus(String taskType);
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/JobServiceImpl.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/JobServiceImpl.java
new file mode 100644
index 0000000..9c3ffdc
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/JobServiceImpl.java
@@ -0,0 +1,478 @@
+/*
+ * 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.paimon.web.server.service.impl;
+
+import org.apache.paimon.web.engine.flink.common.executor.Executor;
+import org.apache.paimon.web.engine.flink.common.executor.ExecutorFactory;
+import org.apache.paimon.web.engine.flink.common.result.ExecutionResult;
+import org.apache.paimon.web.engine.flink.common.result.FetchResultParams;
+import org.apache.paimon.web.engine.flink.common.status.JobStatus;
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+import org.apache.paimon.web.gateway.config.ExecutionConfig;
+import org.apache.paimon.web.gateway.enums.EngineType;
+import org.apache.paimon.web.gateway.provider.ExecutorFactoryProvider;
+import org.apache.paimon.web.server.data.dto.JobSubmitDTO;
+import org.apache.paimon.web.server.data.dto.ResultFetchDTO;
+import org.apache.paimon.web.server.data.dto.SessionDTO;
+import org.apache.paimon.web.server.data.dto.StopJobDTO;
+import org.apache.paimon.web.server.data.model.ClusterInfo;
+import org.apache.paimon.web.server.data.model.JobInfo;
+import org.apache.paimon.web.server.data.vo.JobStatisticsVO;
+import org.apache.paimon.web.server.data.vo.JobVO;
+import org.apache.paimon.web.server.data.vo.ResultDataVO;
+import org.apache.paimon.web.server.mapper.JobMapper;
+import org.apache.paimon.web.server.service.ClusterService;
+import org.apache.paimon.web.server.service.JobExecutorService;
+import org.apache.paimon.web.server.service.JobService;
+import org.apache.paimon.web.server.service.SessionService;
+import org.apache.paimon.web.server.service.UserSessionManager;
+import org.apache.paimon.web.server.util.LocalDateTimeUtil;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/** The implementation of {@link JobService}. */
+@Service
+@Slf4j
+public class JobServiceImpl extends ServiceImpl<JobMapper, JobInfo> implements
JobService {
+
+ private static final String STREAMING_MODE = "Streaming";
+ private static final String BATCH_MODE = "Batch";
+ private static final String SHOW_JOBS_STATEMENT = "SHOW JOBS";
+
+ @Autowired private JobMapper jobMapper;
+
+ @Autowired private UserSessionManager sessionManager;
+
+ @Autowired private SessionService sessionService;
+
+ @Autowired private ClusterService clusterService;
+
+ @Autowired private JobExecutorService jobExecutorService;
+
+ @Override
+ public JobVO submitJob(JobSubmitDTO jobSubmitDTO) {
+ String pipelineName = getPipelineName(jobSubmitDTO.getStatements());
+ if (StringUtils.isNotBlank(pipelineName)) {
+ jobSubmitDTO.setJobName(pipelineName);
+ } else {
+ pipelineName = jobSubmitDTO.getJobName();
+ jobSubmitDTO.setStatements(
+ addPipelineNameStatement(pipelineName,
jobSubmitDTO.getStatements()));
+ }
+
+ Executor executor =
+ this.getExecutor(jobSubmitDTO.getClusterId(),
jobSubmitDTO.getTaskType());
+ if (executor == null) {
+ throw new RuntimeException("No executor available for the job
submission.");
+ }
+
+ try {
+ ExecutionResult executionResult =
executor.executeSql(jobSubmitDTO.getStatements());
+ if (StringUtils.isNotBlank(executionResult.getJobId())) {
+ JobInfo jobInfo = buildJobInfo(executionResult, jobSubmitDTO);
+ this.save(jobInfo);
+ }
+ return buildJobVO(executionResult, jobSubmitDTO);
+ } catch (Exception e) {
+ throw new RuntimeException("Error executing job: " +
e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public ResultDataVO fetchResult(ResultFetchDTO resultFetchDTO) {
+ long token = resultFetchDTO.getToken() + 1;
+ try {
+ Executor executor =
+ this.getExecutor(resultFetchDTO.getClusterId(),
resultFetchDTO.getTaskType());
+ if (executor == null) {
+ throw new RuntimeException("No executor available for result
fetching.");
+ }
+ FetchResultParams params =
+ FetchResultParams.builder()
+ .sessionId(resultFetchDTO.getSessionId())
+ .submitId(resultFetchDTO.getSubmitId())
+ .token(token)
+ .build();
+ ExecutionResult executionResult = executor.fetchResults(params);
+ ResultDataVO.ResultDataVOBuilder builder =
+ ResultDataVO.builder()
+ .resultData(executionResult.getData())
+ .rows(executionResult.getData().size())
+ .token(token);
+ if (!executionResult.getData().isEmpty()) {
+
builder.columns(executionResult.getData().get(0).entrySet().size());
+ } else {
+ builder.columns(0);
+ }
+ return builder.build();
+ } catch (Exception e) {
+ throw new RuntimeException("Error fetching result:" +
e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public List<JobVO> listJobs() {
+ List<JobInfo> jobInfos = this.list();
+ return
jobInfos.stream().map(this::convertJobInfoToJobVO).collect(Collectors.toList());
+ }
+
+ @Override
+ public List<JobVO> listJobsByPage(int current, int size) {
+ QueryWrapper<JobInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.orderByDesc("create_time");
+ Page<JobInfo> page = new Page<>(current, size);
+ IPage<JobInfo> jobInfoIPage = jobMapper.selectPage(page, queryWrapper);
+ List<JobInfo> records = jobInfoIPage.getRecords();
+ return
records.stream().map(this::convertJobInfoToJobVO).collect(Collectors.toList());
+ }
+
+ @Override
+ public JobInfo getJobById(String id) {
+ QueryWrapper<JobInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("job_id", id);
+ return jobMapper.selectOne(queryWrapper);
+ }
+
+ @Override
+ public JobStatisticsVO getJobStatistics() {
+ List<JobInfo> jobInfos = this.list();
+
+ long totalNum = jobInfos.size();
+ long runningNum =
+ jobInfos.stream()
+ .filter(job ->
JobStatus.RUNNING.getValue().equals(job.getStatus()))
+ .count();
+ long finishedNum =
+ jobInfos.stream()
+ .filter(job ->
JobStatus.FINISHED.getValue().equals(job.getStatus()))
+ .count();
+ long canceledNum =
+ jobInfos.stream()
+ .filter(job ->
JobStatus.CANCELED.getValue().equals(job.getStatus()))
+ .count();
+ long failedNum =
+ jobInfos.stream()
+ .filter(job ->
JobStatus.FAILED.getValue().equals(job.getStatus()))
+ .count();
+
+ return JobStatisticsVO.builder()
+ .totalNum(totalNum)
+ .runningNum(runningNum)
+ .finishedNum(finishedNum)
+ .canceledNum(canceledNum)
+ .failedNum(failedNum)
+ .build();
+ }
+
+ @Override
+ public void stop(StopJobDTO stopJobDTO) {
+ try {
+ Executor executor = getExecutor(stopJobDTO.getClusterId(),
stopJobDTO.getTaskType());
+ if (executor == null) {
+ throw new RuntimeException("No executor available for job
stopping.");
+ }
+ executor.stop(stopJobDTO.getJobId(), stopJobDTO.isWithSavepoint());
+ boolean updateStatus =
+ updateJobStatusAndEndTime(
+ stopJobDTO.getJobId(),
+ JobStatus.CANCELED.getValue(),
+ LocalDateTime.now());
+ if (!updateStatus) {
+ log.error(
+ "Failed to update job status in the database for
jobId: {}",
+ stopJobDTO.getJobId());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Error stopping job:" + e.getMessage(),
e);
+ }
+ }
+
+ @Override
+ public void refreshJobStatus(String taskType) {
+ if (!StpUtil.isLogin()) {
+ throw new IllegalStateException("User must be logged in to access
this resource");
+ }
+ int userId = StpUtil.getLoginIdAsInt();
+
+ if (taskType.equals("Flink")) {
+ QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("type", "Flink");
+ List<ClusterInfo> clusters = clusterService.list();
+ for (ClusterInfo cluster : clusters) {
+ try {
+ SessionEntity session =
+ sessionManager.getSession(userId + "_" +
cluster.getId());
+ if (session == null) {
+ return;
+ }
+
+ Executor executor =
jobExecutorService.getExecutor(session.getSessionId());
+ if (executor == null) {
+ return;
+ }
+
+ ExecutionResult executionResult =
executor.executeSql(SHOW_JOBS_STATEMENT);
+ List<Map<String, Object>> jobsData =
executionResult.getData();
+ for (Map<String, Object> jobData : jobsData) {
+ String jobId = (String) jobData.get("job id");
+ String jobStatus = (String) jobData.get("status");
+ String utcTimeString = (String) jobData.get("start
time");
+ LocalDateTime startTime =
+
LocalDateTimeUtil.convertUtcStringToLocalDateTime(utcTimeString);
+ JobInfo job = getJobById(jobId);
+ if (job != null && job.getUid().equals(userId)) {
+ String currentStatus = job.getStatus();
+ if (!jobStatus.equals(currentStatus)) {
+ if
(JobStatus.RUNNING.getValue().equals(jobStatus)) {
+ updateJobStatusAndStartTime(jobId,
jobStatus, startTime);
+ } else if
(JobStatus.FINISHED.getValue().equals(jobStatus)
+ ||
JobStatus.CANCELED.getValue().equals(jobStatus)) {
+ LocalDateTime endTime =
+ job.getEndTime() == null
+ ? LocalDateTime.now()
+ : job.getEndTime();
+ updateJobStatusAndEndTime(jobId,
jobStatus, endTime);
+ }
+ }
+ } else {
+ log.warn("Job with ID {} not found in the
database.", jobId);
+ }
+ }
+ } catch (Exception e) {
+ log.error("Exception with refreshing job status.", e);
+ }
+ }
+ }
+ }
+
+ @Async
+ @Scheduled(initialDelay = 60000, fixedDelay = 30000)
+ public void initializeExecutorsIfNeeded() {
+ List<SessionEntity> sessions = sessionManager.getAllSessions();
+ for (SessionEntity session : sessions) {
+ try {
+ Executor executor =
jobExecutorService.getExecutor(session.getSessionId());
+ if (executor == null) {
+ ExecutionConfig config =
+
ExecutionConfig.builder().sessionEntity(session).build();
+ EngineType engineType = EngineType.fromName("FLINK");
+ ExecutorFactoryProvider provider = new
ExecutorFactoryProvider(config);
+ ExecutorFactory executorFactory =
provider.getExecutorFactory(engineType);
+ executor = executorFactory.createExecutor();
+ jobExecutorService.addExecutor(session.getSessionId(),
executor);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to create executor for session ID:" +
session.getSessionId(), e);
+ }
+ }
+ }
+
+ private boolean updateJobStatusAndEndTime(
+ String jobId, String newStatus, LocalDateTime endTime) {
+ JobInfo jobInfo = new JobInfo();
+ jobInfo.setStatus(newStatus);
+ jobInfo.setEndTime(endTime);
+ return this.update(jobInfo, new QueryWrapper<JobInfo>().eq("job_id",
jobId));
+ }
+
+ private boolean updateJobStatusAndStartTime(
+ String jobId, String newStatus, LocalDateTime startTime) {
+ JobInfo jobInfo = new JobInfo();
+ jobInfo.setStatus(newStatus);
+ jobInfo.setStartTime(startTime);
+ return this.update(jobInfo, new QueryWrapper<JobInfo>().eq("job_id",
jobId));
+ }
+
+ private JobVO convertJobInfoToJobVO(JobInfo jobInfo) {
+ JobVO.JobVOBuilder builder =
+ JobVO.builder()
+ .jobId(jobInfo.getJobId())
+ .jobName(jobInfo.getJobName())
+ .type(jobInfo.getType())
+ .executeMode(jobInfo.getExecuteMode())
+ .clusterId(jobInfo.getClusterId());
+ if (jobInfo.getStartTime() != null) {
+ builder.startTime(jobInfo.getStartTime());
+ }
+ if (jobInfo.getEndTime() != null) {
+ builder.endTime(jobInfo.getEndTime());
+ }
+ if (StringUtils.isNotBlank(jobInfo.getStatus())) {
+ builder.status(jobInfo.getStatus());
+ }
+ return builder.build();
+ }
+
+ private JobInfo buildJobInfo(ExecutionResult executionResult, JobSubmitDTO
jobSubmitDTO) {
+ JobInfo.JobInfoBuilder builder =
+ JobInfo.builder()
+ .jobId(executionResult.getJobId())
+ .type(jobSubmitDTO.getTaskType())
+ .statements(jobSubmitDTO.getStatements())
+ .status(JobStatus.CREATED.getValue())
+ .clusterId(jobSubmitDTO.getClusterId());
+
+ String jobName = jobSubmitDTO.getJobName() != null ?
jobSubmitDTO.getJobName() : "";
+ builder.jobName(jobName);
+
+ if (StringUtils.isNotBlank(executionResult.getStatus())) {
+ builder.status(executionResult.getStatus());
+ }
+
+ Map<String, String> config =
+ MapUtils.isNotEmpty(jobSubmitDTO.getConfig())
+ ? jobSubmitDTO.getConfig()
+ : new HashMap<>();
+ ObjectMapper objectMapper = new ObjectMapper();
+ String jsonConfig;
+ try {
+ jsonConfig = objectMapper.writeValueAsString(config);
+ } catch (Exception e) {
+ jsonConfig = "{}";
+ }
+ builder.config(jsonConfig);
+
+ String executeMode = jobSubmitDTO.isStreaming() ? STREAMING_MODE :
BATCH_MODE;
+ builder.executeMode(executeMode);
+
+ if (StpUtil.isLogin()) {
+ builder.uid(StpUtil.getLoginIdAsInt());
+ }
+ return builder.build();
+ }
+
+ private JobVO buildJobVO(ExecutionResult executionResult, JobSubmitDTO
jobSubmitDTO) {
+ JobVO.JobVOBuilder builder =
+ JobVO.builder()
+ .type(jobSubmitDTO.getTaskType())
+ .shouldFetchResult(executionResult.shouldFetchResult())
+ .submitId(executionResult.getSubmitId())
+ .resultData(executionResult.getData())
+ .clusterId(jobSubmitDTO.getClusterId())
+ .jobName(jobSubmitDTO.getJobName())
+ .token(0L);
+ String executeMode = jobSubmitDTO.isStreaming() ? STREAMING_MODE :
BATCH_MODE;
+ builder.executeMode(executeMode);
+ if (StringUtils.isNotBlank(executionResult.getJobId())) {
+ builder.jobId(executionResult.getJobId());
+ }
+ if (StringUtils.isNotBlank(executionResult.getStatus())) {
+ builder.status(executionResult.getStatus());
+ }
+ if (StpUtil.isLogin()) {
+ builder.uid(StpUtil.getLoginIdAsInt())
+ .sessionId(
+ sessionManager
+ .getSession(
+ StpUtil.getLoginIdAsInt()
+ + "_"
+ +
jobSubmitDTO.getClusterId())
+ .getSessionId());
+ }
+ return builder.build();
+ }
+
+ private String getPipelineName(String statements) {
+ String regex = "set\\s+'pipeline\\.name'\\s*=\\s*'([^']+)'";
+ Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
+ Matcher matcher = pattern.matcher(statements);
+
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return null;
+ }
+
+ private String addPipelineNameStatement(String pipelineName, String
statements) {
+ return "SET 'pipeline.name' = '" + pipelineName + "';\n" + statements;
+ }
+
+ private boolean shouldCreateSession(String clusterId) {
+ if (StpUtil.isLogin()) {
+ SessionEntity session =
+ sessionManager.getSession(StpUtil.getLoginIdAsInt() + "_"
+ clusterId);
+ if (session != null) {
+ SessionDTO sessionDTO = new SessionDTO();
+ sessionDTO.setHost(session.getHost());
+ sessionDTO.setPort(session.getPort());
+ sessionDTO.setUid(StpUtil.getLoginIdAsInt());
+ sessionDTO.setClusterId(Integer.valueOf(clusterId));
+ return sessionService.triggerSessionHeartbeat(sessionDTO) <= 0;
+ }
+ }
+ return true;
+ }
+
+ private Executor getExecutor(String clusterId, String taskType) {
+ try {
+ if (!StpUtil.isLogin()) {
+ throw new IllegalStateException("User must be logged in to
access this resource");
+ }
+ if (shouldCreateSession(clusterId)) {
+ ClusterInfo clusterInfo = clusterService.getById(clusterId);
+ if (clusterInfo == null) {
+ throw new IllegalStateException("No cluster found with ID:
" + clusterId);
+ }
+ SessionDTO sessionDTO = new SessionDTO();
+ sessionDTO.setHost(clusterInfo.getHost());
+ sessionDTO.setPort(clusterInfo.getPort());
+ sessionDTO.setUid(StpUtil.getLoginIdAsInt());
+ sessionDTO.setClusterId(Integer.valueOf(clusterId));
+ sessionService.createSession(sessionDTO);
+ }
+
+ SessionEntity session =
+ sessionManager.getSession(StpUtil.getLoginIdAsInt() + "_"
+ clusterId);
+
+ if (jobExecutorService.getExecutor(session.getSessionId()) ==
null) {
+ ExecutionConfig config =
ExecutionConfig.builder().sessionEntity(session).build();
+ EngineType engineType =
EngineType.fromName(taskType.toUpperCase());
+ ExecutorFactoryProvider provider = new
ExecutorFactoryProvider(config);
+ ExecutorFactory executorFactory =
provider.getExecutorFactory(engineType);
+ Executor executor = executorFactory.createExecutor();
+ jobExecutorService.addExecutor(session.getSessionId(),
executor);
+ }
+ return jobExecutorService.getExecutor(session.getSessionId());
+ } catch (Exception e) {
+ log.error("Failed to create executor: {}", e.getMessage(), e);
+ }
+ return null;
+ }
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/LocalDateTimeUtil.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/LocalDateTimeUtil.java
new file mode 100644
index 0000000..7443f97
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/LocalDateTimeUtil.java
@@ -0,0 +1,36 @@
+/*
+ * 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.paimon.web.server.util;
+
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+/** Local date time util. */
+public class LocalDateTimeUtil {
+
+ public static LocalDateTime convertUtcStringToLocalDateTime(String
utcTimeStr) {
+ OffsetDateTime utcTime =
+ OffsetDateTime.parse(utcTimeStr + "Z",
DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+ ZonedDateTime beijingTime =
utcTime.atZoneSameInstant(ZoneId.of("Asia/Shanghai"));
+ return beijingTime.toLocalDateTime();
+ }
+}
diff --git a/paimon-web-server/src/main/resources/i18n/messages.properties
b/paimon-web-server/src/main/resources/i18n/messages.properties
index 96ae9d1..546bf8d 100644
--- a/paimon-web-server/src/main/resources/i18n/messages.properties
+++ b/paimon-web-server/src/main/resources/i18n/messages.properties
@@ -53,3 +53,7 @@ cdc.job.exist.error=Paimon CDC job exists.
cdc.job.not.exist.error=Paimon CDC job is not exist.
cluster.not.exist=This cluster is not exist.
cluster.name.exist=This cluster name {0} already exists.
+job.submit.error=Exception submitting a job.
+result.fetch.error=Exception fetching result data.
+job.stop.error=Exception stopping a job.
+job.update.status.error=Exception updating the job status.
diff --git
a/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
b/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
index 96ae9d1..546bf8d 100644
--- a/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
+++ b/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
@@ -53,3 +53,7 @@ cdc.job.exist.error=Paimon CDC job exists.
cdc.job.not.exist.error=Paimon CDC job is not exist.
cluster.not.exist=This cluster is not exist.
cluster.name.exist=This cluster name {0} already exists.
+job.submit.error=Exception submitting a job.
+result.fetch.error=Exception fetching result data.
+job.stop.error=Exception stopping a job.
+job.update.status.error=Exception updating the job status.
diff --git
a/paimon-web-server/src/main/resources/i18n/messages_zh_CN.properties
b/paimon-web-server/src/main/resources/i18n/messages_zh_CN.properties
index d61de0c..3ea53d8 100644
--- a/paimon-web-server/src/main/resources/i18n/messages_zh_CN.properties
+++ b/paimon-web-server/src/main/resources/i18n/messages_zh_CN.properties
@@ -53,3 +53,7 @@ cdc.job.exist.error=paimon cdc\u4F5C\u4E1A\u5DF2\u5B58\u5728
cdc.job.not.exist.error=paimon cdc\u4F5C\u4E1A\u4E0D\u5B58\u5728
cluster.not.exist=\u6B64\u96C6\u7FA4\u4E0D\u5B58\u5728
cluster.name.exist=\u6B64\u96C6\u7FA4\u540D{0}\u5DF2\u7ECF\u5B58\u5728
+job.submit.error=\u63D0\u4EA4\u4F5C\u4E1A\u65F6\u53D1\u751F\u5F02\u5E38
+result.fetch.error=\u83B7\u53D6\u7ED3\u679C\u6570\u636E\u65F6\u53D1\u751F\u5F02\u5E38
+job.stop.error=\u505C\u6B62\u4F5C\u4E1A\u65F6\u53D1\u751F\u5F02\u5E38
+job.update.status.error=\u66F4\u65B0\u4F5C\u4E1A\u72B6\u6001\u65F6\u53D1\u751F\u5F02\u5E38
diff --git
a/paimon-web-server/src/test/java/org/apache/paimon/web/server/constant/StatementsConstant.java
b/paimon-web-server/src/test/java/org/apache/paimon/web/server/constant/StatementsConstant.java
new file mode 100644
index 0000000..7d7ce3f
--- /dev/null
+++
b/paimon-web-server/src/test/java/org/apache/paimon/web/server/constant/StatementsConstant.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.constant;
+
+/** Statements constant. */
+public class StatementsConstant {
+
+ public static String statement =
+ "CREATE TABLE IF NOT EXISTS t_order(\n"
+ + " `order_id` BIGINT,\n"
+ + " `product` BIGINT,\n"
+ + " `amount` BIGINT,\n"
+ + " `order_time` as CAST(CURRENT_TIMESTAMP AS
TIMESTAMP(3)),\n"
+ + " WATERMARK FOR order_time AS order_time-INTERVAL '2'
SECOND\n"
+ + ") WITH(\n"
+ + " 'connector' = 'datagen',\n"
+ + " 'rows-per-second' = '1',\n"
+ + " 'fields.order_id.min' = '1',\n"
+ + " 'fields.order_id.max' = '2',\n"
+ + " 'fields.amount.min' = '1',\n"
+ + " 'fields.amount.max' = '10',\n"
+ + " 'fields.product.min' = '1',\n"
+ + " 'fields.product.max' = '2'\n"
+ + ");\n"
+ + "CREATE TABLE IF NOT EXISTS sink_table(\n"
+ + " `product` BIGINT,\n"
+ + " `amount` BIGINT,\n"
+ + " `order_time` TIMESTAMP(3),\n"
+ + " `one_minute_sum` BIGINT\n"
+ + ") WITH('connector' = 'print');\n"
+ + "\n"
+ + "INSERT INTO\n"
+ + " sink_table\n"
+ + "SELECT\n"
+ + " product,\n"
+ + " amount,\n"
+ + " order_time,\n"
+ + " 0 as one_minute_sum\n"
+ + "FROM\n"
+ + " t_order;";
+
+ public static String selectStatement =
+ "CREATE TABLE IF NOT EXISTS t_order(\n"
+ + " `order_id` BIGINT,\n"
+ + " `product` BIGINT,\n"
+ + " `amount` BIGINT,\n"
+ + " `order_time` as CAST(CURRENT_TIMESTAMP AS
TIMESTAMP(3)),\n"
+ + " WATERMARK FOR order_time AS order_time-INTERVAL '2'
SECOND\n"
+ + ") WITH(\n"
+ + " 'connector' = 'datagen',\n"
+ + " 'rows-per-second' = '1',\n"
+ + " 'fields.order_id.min' = '1',\n"
+ + " 'fields.order_id.max' = '2',\n"
+ + " 'fields.amount.min' = '1',\n"
+ + " 'fields.amount.max' = '10',\n"
+ + " 'fields.product.min' = '1',\n"
+ + " 'fields.product.max' = '2'\n"
+ + ");\n"
+ + "SELECT * FROM t_order;";
+}
diff --git
a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/JobControllerTest.java
b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/JobControllerTest.java
new file mode 100644
index 0000000..bac3f46
--- /dev/null
+++
b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/JobControllerTest.java
@@ -0,0 +1,404 @@
+/*
+ * 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.paimon.web.server.controller;
+
+import org.apache.paimon.web.server.constant.StatementsConstant;
+import org.apache.paimon.web.server.data.dto.JobSubmitDTO;
+import org.apache.paimon.web.server.data.dto.LoginDTO;
+import org.apache.paimon.web.server.data.dto.ResultFetchDTO;
+import org.apache.paimon.web.server.data.dto.StopJobDTO;
+import org.apache.paimon.web.server.data.model.ClusterInfo;
+import org.apache.paimon.web.server.data.result.R;
+import org.apache.paimon.web.server.data.vo.JobStatisticsVO;
+import org.apache.paimon.web.server.data.vo.JobStatusVO;
+import org.apache.paimon.web.server.data.vo.JobVO;
+import org.apache.paimon.web.server.data.vo.ResultDataVO;
+import org.apache.paimon.web.server.service.ClusterService;
+import org.apache.paimon.web.server.util.ObjectMapperUtils;
+import org.apache.paimon.web.server.util.StringUtils;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockCookie;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Tests for {@link JobController}. */
+@SpringBootTest
+@AutoConfigureMockMvc
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class JobControllerTest extends FlinkSQLGatewayTestBase {
+
+ private static final String loginPath = "/api/login";
+ private static final String logoutPath = "/api/logout";
+ private static final String jobPath = "/api/job";
+
+ @Value("${spring.application.name}")
+ private String tokenName;
+
+ @Autowired public MockMvc mockMvc;
+
+ public static MockCookie cookie;
+
+ @Autowired private ClusterService clusterService;
+
+ @BeforeEach
+ public void before() throws Exception {
+ LoginDTO login = new LoginDTO();
+ login.setUsername("admin");
+ login.setPassword("admin");
+ MockHttpServletResponse response =
+ mockMvc.perform(
+ MockMvcRequestBuilders.post(loginPath)
+
.content(ObjectMapperUtils.toJSON(login))
+
.contentType(MediaType.APPLICATION_JSON_VALUE)
+
.accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse();
+ String result = response.getContentAsString();
+ R<?> r = ObjectMapperUtils.fromJSON(result, R.class);
+ assertEquals(200, r.getCode());
+
+ assertTrue(StringUtils.isNotBlank(r.getData().toString()));
+
+ cookie = (MockCookie) response.getCookie(tokenName);
+
+ ClusterInfo cluster =
+ ClusterInfo.builder()
+ .clusterName("test_cluster")
+ .host(targetAddress)
+ .port(port)
+ .enabled(true)
+ .type("Flink")
+ .build();
+ boolean res = clusterService.save(cluster);
+ assertTrue(res);
+ }
+
+ @AfterEach
+ public void after() throws Exception {
+ String result =
+ mockMvc.perform(
+ MockMvcRequestBuilders.post(logoutPath)
+ .cookie(cookie)
+
.contentType(MediaType.APPLICATION_JSON_VALUE)
+
.accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ R<?> r = ObjectMapperUtils.fromJSON(result, R.class);
+ assertEquals(200, r.getCode());
+
+ QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("cluster_name", "test_cluster");
+ ClusterInfo one = clusterService.getOne(queryWrapper);
+ int res = clusterService.deleteClusterByIds(new Integer[]
{one.getId()});
+ assertTrue(res > 0);
+ }
+
+ @Test
+ @Order(1)
+ public void testSubmitJob() throws Exception {
+ QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("cluster_name", "test_cluster");
+ ClusterInfo one = clusterService.getOne(queryWrapper);
+ JobSubmitDTO jobSubmitDTO = new JobSubmitDTO();
+ jobSubmitDTO.setJobName("flink-job-test");
+ jobSubmitDTO.setTaskType("Flink");
+ jobSubmitDTO.setClusterId(String.valueOf(one.getId()));
+ jobSubmitDTO.setStatements(StatementsConstant.statement);
+
+ String responseString = submit(jobSubmitDTO);
+
+ R<JobVO> r = ObjectMapperUtils.fromJSON(responseString, new
TypeReference<R<JobVO>>() {});
+ assertEquals(200, r.getCode());
+ assertNotNull(r.getData());
+ assertEquals("flink-job-test", r.getData().getJobName());
+ assertEquals("Flink", r.getData().getType());
+ assertEquals(1, r.getData().getUid());
+ }
+
+ @Test
+ @Order(2)
+ public void testFetchResult() throws Exception {
+ QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("cluster_name", "test_cluster");
+ ClusterInfo one = clusterService.getOne(queryWrapper);
+ JobSubmitDTO jobSubmitDTO = new JobSubmitDTO();
+ jobSubmitDTO.setJobName("flink-job-test-fetch-result");
+ jobSubmitDTO.setTaskType("Flink");
+ jobSubmitDTO.setClusterId(String.valueOf(one.getId()));
+ jobSubmitDTO.setStatements(StatementsConstant.selectStatement);
+
+ String responseString = submit(jobSubmitDTO);
+ R<JobVO> r = ObjectMapperUtils.fromJSON(responseString, new
TypeReference<R<JobVO>>() {});
+ assertEquals(200, r.getCode());
+
+ if (r.getData().getShouldFetchResult()) {
+ ResultFetchDTO resultFetchDTO = new ResultFetchDTO();
+ resultFetchDTO.setSubmitId(r.getData().getSubmitId());
+ resultFetchDTO.setClusterId(r.getData().getClusterId());
+ resultFetchDTO.setSessionId(r.getData().getSessionId());
+ resultFetchDTO.setTaskType(r.getData().getType());
+ resultFetchDTO.setToken(r.getData().getToken());
+
+ String fetchResultString =
+ mockMvc.perform(
+ MockMvcRequestBuilders.post(jobPath +
"/fetch")
+ .cookie(cookie)
+
.content(ObjectMapperUtils.toJSON(resultFetchDTO))
+
.contentType(MediaType.APPLICATION_JSON_VALUE)
+
.accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ R<ResultDataVO> fetchResult =
+ ObjectMapperUtils.fromJSON(
+ fetchResultString, new
TypeReference<R<ResultDataVO>>() {});
+ assertEquals(200, fetchResult.getCode());
+ assertEquals(1, fetchResult.getData().getToken());
+ }
+ }
+
+ @Test
+ @Order(3)
+ public void testListJobs() throws Exception {
+ QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("cluster_name", "test_cluster");
+ ClusterInfo one = clusterService.getOne(queryWrapper);
+ JobSubmitDTO jobSubmitDTO = new JobSubmitDTO();
+ jobSubmitDTO.setJobName("flink-job-test-list-jobs");
+ jobSubmitDTO.setTaskType("Flink");
+ jobSubmitDTO.setClusterId(String.valueOf(one.getId()));
+ jobSubmitDTO.setStatements(StatementsConstant.selectStatement);
+
+ String responseString = submit(jobSubmitDTO);
+ R<JobVO> r = ObjectMapperUtils.fromJSON(responseString, new
TypeReference<R<JobVO>>() {});
+ assertEquals(200, r.getCode());
+
+ String listJobResponseStr =
+ mockMvc.perform(
+ MockMvcRequestBuilders.get(jobPath + "/list")
+ .cookie(cookie)
+
.contentType(MediaType.APPLICATION_JSON_VALUE)
+
.accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ R<List<JobVO>> listJobRes =
+ ObjectMapperUtils.fromJSON(
+ listJobResponseStr, new
TypeReference<R<List<JobVO>>>() {});
+ assertEquals(200, listJobRes.getCode());
+ assertEquals(3, listJobRes.getData().size());
+ String jobs =
+ listJobRes.getData().get(0).getJobName()
+ + ","
+ + listJobRes.getData().get(1).getJobName()
+ + ","
+ + listJobRes.getData().get(2).getJobName();
+ assertTrue(jobs.contains("flink-job-test-list-jobs"));
+ }
+
+ @Test
+ @Order(4)
+ public void testGetJobStatus() throws Exception {
+ QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("cluster_name", "test_cluster");
+ ClusterInfo one = clusterService.getOne(queryWrapper);
+ JobSubmitDTO jobSubmitDTO = new JobSubmitDTO();
+ jobSubmitDTO.setJobName("flink-job-test-get-job-status");
+ jobSubmitDTO.setTaskType("Flink");
+ jobSubmitDTO.setClusterId(String.valueOf(one.getId()));
+ jobSubmitDTO.setStatements(StatementsConstant.selectStatement);
+
+ String responseString = submit(jobSubmitDTO);
+ R<JobVO> r = ObjectMapperUtils.fromJSON(responseString, new
TypeReference<R<JobVO>>() {});
+ assertEquals(200, r.getCode());
+
+ String listJobResponseStr = getJobStatus(r.getData().getJobId());
+ R<JobStatusVO> getJobStatusRes =
+ ObjectMapperUtils.fromJSON(
+ listJobResponseStr, new
TypeReference<R<JobStatusVO>>() {});
+ assertEquals(200, getJobStatusRes.getCode());
+ assertEquals(r.getData().getJobId(),
getJobStatusRes.getData().getJobId());
+ assertNotNull(getJobStatusRes.getData().getStatus());
+ }
+
+ @Test
+ @Order(5)
+ public void testStopJob() throws Exception {
+ QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("cluster_name", "test_cluster");
+ ClusterInfo one = clusterService.getOne(queryWrapper);
+ JobSubmitDTO jobSubmitDTO = new JobSubmitDTO();
+ jobSubmitDTO.setJobName("flink-job-test-stop-job");
+ jobSubmitDTO.setTaskType("Flink");
+ jobSubmitDTO.setClusterId(String.valueOf(one.getId()));
+ jobSubmitDTO.setStatements(StatementsConstant.selectStatement);
+ String responseString = submit(jobSubmitDTO);
+ R<JobVO> r = ObjectMapperUtils.fromJSON(responseString, new
TypeReference<R<JobVO>>() {});
+ assertEquals(200, r.getCode());
+
+ String jobStatus = getJobStatus(r.getData().getJobId());
+ R<JobStatusVO> getJobStatusRes =
+ ObjectMapperUtils.fromJSON(jobStatus, new
TypeReference<R<JobStatusVO>>() {});
+ while (!getJobStatusRes.getData().getStatus().equals("RUNNING")) {
+ refreshJob();
+ jobStatus = getJobStatus(r.getData().getJobId());
+ getJobStatusRes =
+ ObjectMapperUtils.fromJSON(jobStatus, new
TypeReference<R<JobStatusVO>>() {});
+ TimeUnit.SECONDS.sleep(1);
+ }
+
+ StopJobDTO stopJobDTO = new StopJobDTO();
+ stopJobDTO.setJobId(r.getData().getJobId());
+ stopJobDTO.setClusterId(r.getData().getClusterId());
+ stopJobDTO.setTaskType(r.getData().getType());
+ String stopJobString =
+ mockMvc.perform(
+ MockMvcRequestBuilders.post(jobPath + "/stop")
+ .cookie(cookie)
+
.content(ObjectMapperUtils.toJSON(stopJobDTO))
+
.contentType(MediaType.APPLICATION_JSON_VALUE)
+
.accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ R<Void> stopJobRes =
+ ObjectMapperUtils.fromJSON(stopJobString, new
TypeReference<R<Void>>() {});
+ assertEquals(200, stopJobRes.getCode());
+
+ jobStatus = getJobStatus(r.getData().getJobId());
+ getJobStatusRes =
+ ObjectMapperUtils.fromJSON(jobStatus, new
TypeReference<R<JobStatusVO>>() {});
+ assertEquals(200, getJobStatusRes.getCode());
+ assertEquals("CANCELED", getJobStatusRes.getData().getStatus());
+ }
+
+ @Test
+ @Order(6)
+ public void testGetJobStatistics() throws Exception {
+ QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("cluster_name", "test_cluster");
+ ClusterInfo one = clusterService.getOne(queryWrapper);
+ JobSubmitDTO jobSubmitDTO = new JobSubmitDTO();
+ jobSubmitDTO.setJobName("flink-job-test-get-job-statistics");
+ jobSubmitDTO.setTaskType("Flink");
+ jobSubmitDTO.setClusterId(String.valueOf(one.getId()));
+ jobSubmitDTO.setStatements(StatementsConstant.selectStatement);
+ String responseString = submit(jobSubmitDTO);
+ R<JobVO> r = ObjectMapperUtils.fromJSON(responseString, new
TypeReference<R<JobVO>>() {});
+ assertEquals(200, r.getCode());
+ refreshJob();
+ TimeUnit.MICROSECONDS.sleep(100);
+ refreshJob();
+
+ String getJobStatisticsResponseStr =
+ mockMvc.perform(
+ MockMvcRequestBuilders.get(jobPath +
"/statistics/get")
+ .cookie(cookie)
+
.contentType(MediaType.APPLICATION_JSON_VALUE)
+
.accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ R<JobStatisticsVO> getJobStatisticsRes =
+ ObjectMapperUtils.fromJSON(
+ getJobStatisticsResponseStr, new
TypeReference<R<JobStatisticsVO>>() {});
+ assertEquals(200, getJobStatisticsRes.getCode());
+ assertEquals(6, getJobStatisticsRes.getData().getTotalNum());
+ assertEquals(5, getJobStatisticsRes.getData().getRunningNum());
+ assertEquals(1, getJobStatisticsRes.getData().getCanceledNum());
+ assertEquals(0, getJobStatisticsRes.getData().getFinishedNum());
+ assertEquals(0, getJobStatisticsRes.getData().getFailedNum());
+ }
+
+ private String submit(JobSubmitDTO jobSubmitDTO) throws Exception {
+ return mockMvc.perform(
+ MockMvcRequestBuilders.post(jobPath + "/submit")
+ .cookie(cookie)
+
.content(ObjectMapperUtils.toJSON(jobSubmitDTO))
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ }
+
+ private String getJobStatus(String jobId) throws Exception {
+ return mockMvc.perform(
+ MockMvcRequestBuilders.get(jobPath + "/status/get/" +
jobId)
+ .cookie(cookie)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ }
+
+ private void refreshJob() throws Exception {
+ mockMvc.perform(
+ MockMvcRequestBuilders.post(jobPath + "/refresh")
+ .cookie(cookie)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andDo(MockMvcResultHandlers.print())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ }
+}
diff --git a/scripts/sql/paimon-mysql.sql b/scripts/sql/paimon-mysql.sql
index 91f234f..5cc084e 100644
--- a/scripts/sql/paimon-mysql.sql
+++ b/scripts/sql/paimon-mysql.sql
@@ -155,6 +155,24 @@ CREATE TABLE if not exists `cdc_job_definition`
unique (`name`)
) engine = innodb;
+CREATE TABLE if not exists `job`
+(
+ `id` int(11) not null auto_increment primary key comment 'id',
+ `job_id` varchar(100) not null comment 'job id',
+ `job_name` varchar(200) comment 'job name',
+ `type` varchar(100) comment 'job type',
+ `execute_mode` varchar(50) comment 'execute mode',
+ `cluster_id` varchar(100) comment 'cluster id',
+ `uid` INT(11) COMMENT 'User ID',
+ `config` text comment 'config',
+ `statements` text COMMENT 'statements',
+ `status` varchar(50) COMMENT 'status',
+ `start_time` datetime(0) NULL COMMENT 'start time',
+ `end_time` datetime(0) NULL COMMENT 'end time',
+ `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create
time',
+ `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update
time'
+ ) ENGINE = InnoDB DEFAULT CHARSET=utf8;
+
INSERT INTO `user` ( id, username, password, nickname, mobile
, email, enabled, is_delete)
VALUES ( 1, 'admin', '21232f297a57a5a743894a0e4a801fc3', 'Admin', 0