From 723126b52bd9441cee7c645c2bf06099db554a5d Mon Sep 17 00:00:00 2001 From: He Wang Date: Fri, 29 Mar 2024 10:53:15 +0800 Subject: [PATCH 001/165] hotfix(ui): update datasourceTypes following the DbType in spi module (#15776) --- .../components/node/fields/use-datasource.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts index 87dff0de58b4..8b4ef0b897ec 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts @@ -84,12 +84,12 @@ export function useDatasource( disabled: false }, { - id: 9, + id: 10, code: 'REDSHIFT', disabled: false }, { - id: 10, + id: 11, code: 'ATHENA', disabled: false }, @@ -114,12 +114,22 @@ export function useDatasource( disabled: false }, { - id: 15, + id: 16, + code: 'OCEANBASE', + disabled: false + }, + { + id: 17, code: 'SSH', disabled: true }, { - id: 16, + id: 18, + code: 'KYUUBI', + disabled: false + }, + { + id: 19, code: 'DATABEND', disabled: false }, @@ -133,11 +143,6 @@ export function useDatasource( code: 'HANA', disabled: false }, - { - id: 23, - code: 'ZEPPELIN', - disabled: false - }, { id: 23, code: 'DORIS', @@ -145,12 +150,12 @@ export function useDatasource( }, { id: 24, - code: 'SAGEMAKER', + code: 'ZEPPELIN', disabled: false }, { id: 25, - code: 'KYUUBI', + code: 'SAGEMAKER', disabled: false } ] From ae1fe84e85bc140f92d30da001f548d229be66be Mon Sep 17 00:00:00 2001 From: Evan Sun Date: Fri, 29 Mar 2024 13:28:55 +0800 Subject: [PATCH 002/165] [TEST] fill up alert group service test (#15777) Co-authored-by: abzymeinsjtu Co-authored-by: Eric Gao --- .../api/service/AlertGroupServiceTest.java | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/AlertGroupServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/AlertGroupServiceTest.java index 85cbcbc833bc..6571d8b0f8c1 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/AlertGroupServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/AlertGroupServiceTest.java @@ -22,6 +22,7 @@ import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_GROUP_CREATE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_GROUP_DELETE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_GROUP_UPDATE; +import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_GROUP_VIEW; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -41,6 +42,7 @@ import org.apache.commons.collections4.CollectionUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Assertions; @@ -67,6 +69,12 @@ public class AlertGroupServiceTest { private static final Logger baseServiceLogger = LoggerFactory.getLogger(BaseServiceImpl.class); private static final Logger logger = LoggerFactory.getLogger(AlertGroupServiceTest.class); private static final Logger alertGroupServiceLogger = LoggerFactory.getLogger(AlertGroupServiceImpl.class); + private String tooLongDescription = + "this is a toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + + + "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + + + "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo long description"; @InjectMocks private AlertGroupServiceImpl alertGroupService; @@ -81,10 +89,25 @@ public class AlertGroupServiceTest { @Test public void testQueryAlertGroup() { + User user = getLoginUser(); when(alertGroupMapper.queryAllGroupList()).thenReturn(getList()); - List alertGroups = alertGroupService.queryAllAlertGroup(getLoginUser()); + List alertGroups = alertGroupService.queryAllAlertGroup(user); Assertions.assertEquals(2, alertGroups.size()); + + user.setUserType(UserType.GENERAL_USER); + user.setId(2); + + when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.ALERT_GROUP, 2, + alertGroupServiceLogger)) + .thenReturn(Collections.emptySet()); + Assertions.assertEquals(alertGroupService.queryAllAlertGroup(user).size(), 0); + + user.setId(3); + when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.ALERT_GROUP, 3, + alertGroupServiceLogger)) + .thenReturn(Collections.singleton(1)); + assertDoesNotThrow(() -> alertGroupService.queryAllAlertGroup(user)); } @Test @@ -95,6 +118,35 @@ public void testQueryNormalAlertGroup() { Assertions.assertEquals(1, alertGroups.size()); } + @Test + public void testQueryAlertGroupById() { + User user = getLoginUser(); + user.setId(2); + user.setUserType(UserType.GENERAL_USER); + + when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_GROUP, 2, ALERT_GROUP_VIEW, + baseServiceLogger)) + .thenReturn(false); + + assertThrowsServiceException(Status.USER_NO_OPERATION_PERM, + () -> alertGroupService.queryAlertGroupById(user, 1)); + + user.setId(1); + when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_GROUP, 1, ALERT_GROUP_VIEW, + baseServiceLogger)) + .thenReturn(true); + when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.ALERT_GROUP, new Object[]{999}, 1, + baseServiceLogger)) + .thenReturn(true); + when(alertGroupMapper.selectById(999)).thenReturn(null); + + assertThrowsServiceException(Status.ALERT_GROUP_NOT_EXIST, + () -> alertGroupService.queryAlertGroupById(user, 999)); + + when(alertGroupMapper.selectById(999)).thenReturn(getEntity()); + assertDoesNotThrow(() -> alertGroupService.queryAlertGroupById(user, 999)); + } + @Test public void testListPaging() { IPage page = new Page<>(1, 10); @@ -114,6 +166,18 @@ public void testListPaging() { alertGroupPageInfo = alertGroupService.listPaging(user, groupName, 1, 10); Assertions.assertTrue(CollectionUtils.isNotEmpty(alertGroupPageInfo.getTotalList())); + user.setUserType(UserType.GENERAL_USER); + user.setId(99); + page.setTotal(1); + page.setRecords(Collections.singletonList(getEntity())); + + when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.ALERT_GROUP, user.getId(), + alertGroupServiceLogger)) + .thenReturn(Collections.singleton(1)); + when(alertGroupMapper.queryAlertGroupPageByIds(any(Page.class), any(List.class), eq(groupName))) + .thenReturn(page); + + alertGroupService.listPaging(user, groupName, 1, 10).getTotal(); } @Test @@ -134,8 +198,19 @@ public void testCreateAlertgroup() { ALERT_GROUP_CREATE, baseServiceLogger)).thenReturn(true); when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.ALERT_GROUP, null, user.getId(), baseServiceLogger)).thenReturn(true); + + assertThrowsServiceException(Status.DESCRIPTION_TOO_LONG_ERROR, + () -> alertGroupService.createAlertGroup(user, groupName, tooLongDescription, null)); AlertGroup alertGroup = alertGroupService.createAlertGroup(user, groupName, groupName, null); assertNotNull(alertGroup); + + when(alertGroupMapper.insert(any(AlertGroup.class))).thenReturn(-1); + assertThrowsServiceException(Status.CREATE_ALERT_GROUP_ERROR, + () -> alertGroupService.createAlertGroup(user, groupName, groupName, null)); + + when(alertGroupMapper.insert(any(AlertGroup.class))).thenThrow(DuplicateKeyException.class); + assertThrowsServiceException(Status.ALERT_GROUP_EXIST, + () -> alertGroupService.createAlertGroup(user, groupName, groupName, null)); } @Test @@ -162,7 +237,7 @@ public void testUpdateAlertgroup() { user.setUserType(UserType.GENERAL_USER); assertThrowsServiceException(Status.USER_NO_OPERATION_PERM, () -> alertGroupService.updateAlertGroupById(user, 1, groupName, groupName, null)); - user.setUserType(UserType.ADMIN_USER); + // not exist user.setUserType(UserType.ADMIN_USER); when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_GROUP, user.getId(), @@ -171,6 +246,10 @@ public void testUpdateAlertgroup() { baseServiceLogger)).thenReturn(true); assertThrowsServiceException(Status.ALERT_GROUP_NOT_EXIST, () -> alertGroupService.updateAlertGroupById(user, 1, groupName, groupName, null)); + + assertThrowsServiceException(Status.DESCRIPTION_TOO_LONG_ERROR, + () -> alertGroupService.updateAlertGroupById(user, 1, groupName, tooLongDescription, null)); + // success when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.ALERT_GROUP, new Object[]{3}, user.getId(), baseServiceLogger)).thenReturn(true); From dc4dad135c9ccacd376dfa139f174eb0dd086b24 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Sat, 30 Mar 2024 09:43:42 +0800 Subject: [PATCH 003/165] Fix TaskGroupCoordinator might cause OOM when there is a lot of waiting TaskGroupQueue (#15773) --- .../dao/mapper/TaskGroupQueueMapper.java | 14 ++++ .../dao/repository/TaskGroupQueueDao.java | 31 ++++++++ .../impl/TaskGroupQueueDaoImpl.java | 22 ++++++ .../dao/mapper/TaskGroupQueueMapper.xml | 27 +++++++ .../impl/TaskGroupQueueDaoImplTest.java | 76 +++++++++++++++++++ .../taskgroup/TaskGroupCoordinator.java | 60 +++++++++++++-- 6 files changed, 222 insertions(+), 8 deletions(-) diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/TaskGroupQueueMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/TaskGroupQueueMapper.java index cada1c7092cf..8b8241a2ad91 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/TaskGroupQueueMapper.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/TaskGroupQueueMapper.java @@ -122,4 +122,18 @@ List queryUsingTaskGroupQueueByGroupId(@Param("taskGroupId") Int @Param("status") int status, @Param("inQueue") int inQueue, @Param("forceStart") int forceStart); + + int countUsingTaskGroupQueueByGroupId(@Param("taskGroupId") Integer taskGroupId, + @Param("status") int status, + @Param("inQueue") int inQueue, + @Param("forceStart") int forceStart); + + List queryInQueueTaskGroupQueue(@Param("inQueue") int inQueue, + @Param("minTaskGroupQueueId") int minTaskGroupQueueId, + @Param("limit") int limit); + + List queryWaitNotifyForceStartTaskGroupQueue(@Param("inQueue") int inQueue, + @Param("forceStart") int forceStart, + @Param("minTaskGroupQueueId") int minTaskGroupQueueId, + @Param("limit") int limit); } diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/TaskGroupQueueDao.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/TaskGroupQueueDao.java index a788b29bb116..468b7b758cee 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/TaskGroupQueueDao.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/TaskGroupQueueDao.java @@ -38,6 +38,17 @@ public interface TaskGroupQueueDao extends IDao { */ List queryAllInQueueTaskGroupQueue(); + /** + * Query all {@link TaskGroupQueue} which + * in_queue is {@link org.apache.dolphinscheduler.common.enums.Flag#YES} + * and id > minTaskGroupQueueId + * ordered by id asc + * limit #{limit} + * + * @return TaskGroupQueue ordered by id asc + */ + List queryInQueueTaskGroupQueue(int minTaskGroupQueueId, int limit); + /** * Query all {@link TaskGroupQueue} which in_queue is {@link org.apache.dolphinscheduler.common.enums.Flag#YES} and taskGroupId is taskGroupId * @@ -61,4 +72,24 @@ public interface TaskGroupQueueDao extends IDao { * @return TaskGroupQueue */ List queryAcquiredTaskGroupQueueByGroupId(Integer taskGroupId); + + /** + * Count all {@link TaskGroupQueue} which status is TaskGroupQueueStatus.ACQUIRE_SUCCESS and forceStart is {@link org.apache.dolphinscheduler.common.enums.Flag#NO}. + * + * @param taskGroupId taskGroupId + * @return TaskGroupQueue + */ + int countUsingTaskGroupQueueByGroupId(Integer taskGroupId); + + /** + * Query all {@link TaskGroupQueue} which + * in_queue is {@link org.apache.dolphinscheduler.common.enums.Flag#YES} + * and forceStart is {@link org.apache.dolphinscheduler.common.enums.Flag#YES} + * and id > minTaskGroupQueueId + * order by id asc + * limit #{limit} + * + * @return TaskGroupQueue ordered by priority desc + */ + List queryWaitNotifyForceStartTaskGroupQueue(int minTaskGroupQueueId, int limit); } diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/TaskGroupQueueDaoImpl.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/TaskGroupQueueDaoImpl.java index a1808a909183..5fd50deaae07 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/TaskGroupQueueDaoImpl.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/TaskGroupQueueDaoImpl.java @@ -52,6 +52,11 @@ public List queryAllInQueueTaskGroupQueue() { return mybatisMapper.queryAllTaskGroupQueueByInQueue(Flag.YES.getCode()); } + @Override + public List queryInQueueTaskGroupQueue(int minTaskGroupQueueId, int limit) { + return mybatisMapper.queryInQueueTaskGroupQueue(Flag.YES.getCode(), minTaskGroupQueueId, limit); + } + @Override public List queryAllInQueueTaskGroupQueueByGroupId(Integer taskGroupId) { return mybatisMapper.queryAllInQueueTaskGroupQueueByGroupId(taskGroupId, Flag.YES.getCode()); @@ -70,4 +75,21 @@ public List queryAcquiredTaskGroupQueueByGroupId(Integer taskGro Flag.YES.getCode(), Flag.NO.getCode()); } + + @Override + public int countUsingTaskGroupQueueByGroupId(Integer taskGroupId) { + return mybatisMapper.countUsingTaskGroupQueueByGroupId(taskGroupId, + TaskGroupQueueStatus.ACQUIRE_SUCCESS.getCode(), + Flag.YES.ordinal(), + Flag.NO.getCode()); + } + + @Override + public List queryWaitNotifyForceStartTaskGroupQueue(int minTaskGroupQueueId, int limit) { + return mybatisMapper.queryWaitNotifyForceStartTaskGroupQueue( + Flag.YES.getCode(), + Flag.YES.getCode(), + minTaskGroupQueueId, + limit); + } } diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/TaskGroupQueueMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/TaskGroupQueueMapper.xml index 790ad7bfaef0..800e82e8479d 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/TaskGroupQueueMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/TaskGroupQueueMapper.xml @@ -219,6 +219,16 @@ where in_queue = #{inQueue} order by priority desc + + + + + + diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/TaskGroupQueueDaoImplTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/TaskGroupQueueDaoImplTest.java index 17c15371845b..13dcf91f55d1 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/TaskGroupQueueDaoImplTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/TaskGroupQueueDaoImplTest.java @@ -27,7 +27,11 @@ import org.apache.dolphinscheduler.dao.entity.TaskGroupQueue; import org.apache.dolphinscheduler.dao.repository.TaskGroupQueueDao; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.RandomUtils; + import java.util.Date; +import java.util.List; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; @@ -55,6 +59,35 @@ void queryAllInQueueTaskGroupQueue() { assertEquals(1, taskGroupQueueDao.queryAllInQueueTaskGroupQueue().size()); } + @Test + void queryInQueueTaskGroupQueue_withMinId() { + // Insert 1w ~ 10w records + int insertCount = RandomUtils.nextInt(10000, 100000); + List insertTaskGroupQueue = Lists.newArrayList(); + for (int i = 0; i < insertCount; i++) { + TaskGroupQueue taskGroupQueue = createTaskGroupQueue(Flag.NO, TaskGroupQueueStatus.ACQUIRE_SUCCESS); + insertTaskGroupQueue.add(taskGroupQueue); + } + taskGroupQueueDao.insertBatch(insertTaskGroupQueue); + + int minTaskGroupQueueId = -1; + int limit = 1000; + int queryCount = 0; + while (true) { + List taskGroupQueues = + taskGroupQueueDao.queryInQueueTaskGroupQueue(minTaskGroupQueueId, limit); + if (CollectionUtils.isEmpty(taskGroupQueues)) { + break; + } + queryCount += taskGroupQueues.size(); + if (taskGroupQueues.size() < limit) { + break; + } + minTaskGroupQueueId = taskGroupQueues.get(taskGroupQueues.size() - 1).getId(); + } + assertEquals(insertCount, queryCount); + } + @Test void queryAllInQueueTaskGroupQueueByGroupId() { TaskGroupQueue taskGroupQueue = createTaskGroupQueue(Flag.NO, TaskGroupQueueStatus.ACQUIRE_SUCCESS); @@ -91,6 +124,49 @@ void queryUsingTaskGroupQueueByGroupId() { assertEquals(1, taskGroupQueueDao.queryAcquiredTaskGroupQueueByGroupId(1).size()); } + @Test + void countUsingTaskGroupQueueByGroupId() { + assertEquals(0, taskGroupQueueDao.countUsingTaskGroupQueueByGroupId(1)); + + TaskGroupQueue taskGroupQueue = createTaskGroupQueue(Flag.NO, TaskGroupQueueStatus.ACQUIRE_SUCCESS); + taskGroupQueueDao.insert(taskGroupQueue); + assertEquals(1, taskGroupQueueDao.countUsingTaskGroupQueueByGroupId(1)); + + taskGroupQueue = createTaskGroupQueue(Flag.YES, TaskGroupQueueStatus.WAIT_QUEUE); + taskGroupQueueDao.insert(taskGroupQueue); + assertEquals(1, taskGroupQueueDao.countUsingTaskGroupQueueByGroupId(1)); + } + + @Test + void queryWaitNotifyForceStartTaskGroupQueue() { + // Insert 1w records + int insertCount = RandomUtils.nextInt(10000, 20000); + List insertTaskGroupQueue = Lists.newArrayList(); + for (int i = 0; i < insertCount; i++) { + TaskGroupQueue taskGroupQueue = createTaskGroupQueue(Flag.YES, TaskGroupQueueStatus.ACQUIRE_SUCCESS); + + insertTaskGroupQueue.add(taskGroupQueue); + } + taskGroupQueueDao.insertBatch(insertTaskGroupQueue); + + int beginTaskGroupQueueId = -1; + int limit = 1000; + int queryCount = 0; + while (true) { + List taskGroupQueues = + taskGroupQueueDao.queryWaitNotifyForceStartTaskGroupQueue(beginTaskGroupQueueId, limit); + if (CollectionUtils.isEmpty(taskGroupQueues)) { + break; + } + queryCount += taskGroupQueues.size(); + if (taskGroupQueues.size() < limit) { + break; + } + beginTaskGroupQueueId = taskGroupQueues.get(taskGroupQueues.size() - 1).getId(); + } + assertEquals(insertCount, queryCount); + } + private TaskGroupQueue createTaskGroupQueue(Flag forceStart, TaskGroupQueueStatus taskGroupQueueStatus) { return TaskGroupQueue.builder() .taskId(1) diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/taskgroup/TaskGroupCoordinator.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/taskgroup/TaskGroupCoordinator.java index fae0d2b91fd1..bd7af94611ea 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/taskgroup/TaskGroupCoordinator.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/taskgroup/TaskGroupCoordinator.java @@ -96,6 +96,8 @@ public class TaskGroupCoordinator extends BaseDaemonThread { @Autowired private ProcessInstanceDao processInstanceDao; + private static int DEFAULT_LIMIT = 1000; + public TaskGroupCoordinator() { super("TaskGroupCoordinator"); } @@ -147,10 +149,10 @@ private void amendTaskGroupUseSize() { if (CollectionUtils.isEmpty(taskGroups)) { return; } + StopWatch taskGroupCoordinatorRoundTimeCost = StopWatch.createStarted(); + for (TaskGroup taskGroup : taskGroups) { - List taskGroupQueues = - taskGroupQueueDao.queryAcquiredTaskGroupQueueByGroupId(taskGroup.getId()); - int actualUseSize = taskGroupQueues.size(); + int actualUseSize = taskGroupQueueDao.countUsingTaskGroupQueueByGroupId(taskGroup.getId()); if (taskGroup.getUseSize() == actualUseSize) { continue; } @@ -160,13 +162,35 @@ private void amendTaskGroupUseSize() { taskGroup.setUseSize(actualUseSize); taskGroupDao.updateById(taskGroup); } + log.info("Success amend TaskGroup useSize cost: {}/ms", taskGroupCoordinatorRoundTimeCost.getTime()); } /** * Make sure the TaskGroupQueue status is {@link TaskGroupQueueStatus#RELEASE} when the related {@link TaskInstance} is not exist or status is finished. */ private void amendTaskGroupQueueStatus() { - List taskGroupQueues = taskGroupQueueDao.queryAllInQueueTaskGroupQueue(); + int minTaskGroupQueueId = -1; + int limit = DEFAULT_LIMIT; + StopWatch taskGroupCoordinatorRoundTimeCost = StopWatch.createStarted(); + while (true) { + List taskGroupQueues = + taskGroupQueueDao.queryInQueueTaskGroupQueue(minTaskGroupQueueId, limit); + if (CollectionUtils.isEmpty(taskGroupQueues)) { + break; + } + amendTaskGroupQueueStatus(taskGroupQueues); + if (taskGroupQueues.size() < limit) { + break; + } + minTaskGroupQueueId = taskGroupQueues.get(taskGroupQueues.size() - 1).getId(); + } + log.info("Success amend TaskGroupQueue status cost: {}/ms", taskGroupCoordinatorRoundTimeCost.getTime()); + } + + /** + * Make sure the TaskGroupQueue status is {@link TaskGroupQueueStatus#RELEASE} when the related {@link TaskInstance} is not exist or status is finished. + */ + private void amendTaskGroupQueueStatus(List taskGroupQueues) { List taskInstanceIds = taskGroupQueues.stream() .map(TaskGroupQueue::getTaskId) .collect(Collectors.toList()); @@ -198,10 +222,30 @@ private void dealWithForceStartTaskGroupQueue() { // Find the force start task group queue(Which is inQueue and forceStart is YES) // Notify the related waiting task instance // Set the taskGroupQueue status to RELEASE and remove it from queue - List taskGroupQueues = taskGroupQueueDao.queryAllInQueueTaskGroupQueue() - .stream() - .filter(taskGroupQueue -> Flag.YES.getCode() == taskGroupQueue.getForceStart()) - .collect(Collectors.toList()); + // We use limit here to avoid OOM, and we will retry to notify force start queue at next time + int minTaskGroupQueueId = -1; + int limit = DEFAULT_LIMIT; + StopWatch taskGroupCoordinatorRoundTimeCost = StopWatch.createStarted(); + while (true) { + List taskGroupQueues = + taskGroupQueueDao.queryWaitNotifyForceStartTaskGroupQueue(minTaskGroupQueueId, limit); + if (CollectionUtils.isEmpty(taskGroupQueues)) { + break; + } + dealWithForceStartTaskGroupQueue(taskGroupQueues); + if (taskGroupQueues.size() < limit) { + break; + } + minTaskGroupQueueId = taskGroupQueues.get(taskGroupQueues.size() - 1).getId(); + } + log.info("Success deal with force start TaskGroupQueue cost: {}/ms", + taskGroupCoordinatorRoundTimeCost.getTime()); + } + + private void dealWithForceStartTaskGroupQueue(List taskGroupQueues) { + // Find the force start task group queue(Which is inQueue and forceStart is YES) + // Notify the related waiting task instance + // Set the taskGroupQueue status to RELEASE and remove it from queue for (TaskGroupQueue taskGroupQueue : taskGroupQueues) { try { LogUtils.setTaskInstanceIdMDC(taskGroupQueue.getTaskId()); From d39bdcb165c069792fd33cad5c1fb441f7c5ae47 Mon Sep 17 00:00:00 2001 From: John Huang Date: Sun, 31 Mar 2024 21:47:26 +0800 Subject: [PATCH 004/165] [RemoteLogging] Move init into loghandler (#15780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 旺阳 --- .../log/remote/GcsRemoteLogHandler.java | 21 +++++++------------ .../log/remote/OssRemoteLogHandler.java | 17 ++++++--------- .../common/log/remote/S3RemoteLogHandler.java | 19 +++++++---------- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/GcsRemoteLogHandler.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/GcsRemoteLogHandler.java index 20fd30336e94..ad6e534251e6 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/GcsRemoteLogHandler.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/GcsRemoteLogHandler.java @@ -49,19 +49,6 @@ public class GcsRemoteLogHandler implements RemoteLogHandler, Closeable { private static GcsRemoteLogHandler instance; private GcsRemoteLogHandler() { - - } - - public static synchronized GcsRemoteLogHandler getInstance() { - if (instance == null) { - instance = new GcsRemoteLogHandler(); - instance.init(); - } - - return instance; - } - - public void init() { try { credential = readCredentials(); bucketName = readBucketName(); @@ -73,6 +60,14 @@ public void init() { } } + public static synchronized GcsRemoteLogHandler getInstance() { + if (instance == null) { + instance = new GcsRemoteLogHandler(); + } + + return instance; + } + @Override public void close() throws IOException { try { diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/OssRemoteLogHandler.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/OssRemoteLogHandler.java index 792085b19478..59b139451520 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/OssRemoteLogHandler.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/OssRemoteLogHandler.java @@ -44,28 +44,23 @@ public class OssRemoteLogHandler implements RemoteLogHandler, Closeable { private static OssRemoteLogHandler instance; private OssRemoteLogHandler() { + String accessKeyId = readOssAccessKeyId(); + String accessKeySecret = readOssAccessKeySecret(); + String endpoint = readOssEndpoint(); + ossClient = OssClientFactory.buildOssClient(new OssConnection(accessKeyId, accessKeySecret, endpoint)); + bucketName = readOssBucketName(); + checkBucketNameExists(bucketName); } public static synchronized OssRemoteLogHandler getInstance() { if (instance == null) { instance = new OssRemoteLogHandler(); - instance.init(); } return instance; } - public void init() { - String accessKeyId = readOssAccessKeyId(); - String accessKeySecret = readOssAccessKeySecret(); - String endpoint = readOssEndpoint(); - ossClient = OssClientFactory.buildOssClient(new OssConnection(accessKeyId, accessKeySecret, endpoint)); - - bucketName = readOssBucketName(); - checkBucketNameExists(bucketName); - } - @Override public void sendRemoteLog(String logPath) { String objectName = RemoteLogUtils.getObjectNameFromLogPath(logPath); diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/S3RemoteLogHandler.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/S3RemoteLogHandler.java index d1c8c41445ba..54dba2d5bdc7 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/S3RemoteLogHandler.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/S3RemoteLogHandler.java @@ -56,28 +56,23 @@ public class S3RemoteLogHandler implements RemoteLogHandler, Closeable { private static S3RemoteLogHandler instance; private S3RemoteLogHandler() { - + accessKeyId = readAccessKeyID(); + accessKeySecret = readAccessKeySecret(); + region = readRegion(); + bucketName = readBucketName(); + endPoint = readEndPoint(); + s3Client = buildS3Client(); + checkBucketNameExists(bucketName); } public static synchronized S3RemoteLogHandler getInstance() { if (instance == null) { instance = new S3RemoteLogHandler(); - instance.init(); } return instance; } - public void init() { - accessKeyId = readAccessKeyID(); - accessKeySecret = readAccessKeySecret(); - region = readRegion(); - bucketName = readBucketName(); - endPoint = readEndPoint(); - s3Client = buildS3Client(); - checkBucketNameExists(bucketName); - } - protected AmazonS3 buildS3Client() { if (StringUtils.isNotEmpty(endPoint)) { return AmazonS3ClientBuilder From ac0189a636ea91715f2fbaf1e79e8e9e27b07bee Mon Sep 17 00:00:00 2001 From: John Huang Date: Mon, 1 Apr 2024 09:50:49 +0800 Subject: [PATCH 005/165] [DSIP-24][RemoteLogging]Support AbsRemoteLogHandler (#15769) --- docs/docs/en/guide/remote-logging.md | 12 +- docs/docs/zh/guide/remote-logging.md | 12 +- dolphinscheduler-common/pom.xml | 5 + .../common/constants/Constants.java | 7 + .../log/remote/AbsRemoteLogHandler.java | 137 +++++++++++++++++ .../log/remote/RemoteLogHandlerFactory.java | 2 + .../src/main/resources/common.properties | 7 +- .../log/remote/AbsRemoteLogHandlerTest.java | 144 ++++++++++++++++++ 8 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/AbsRemoteLogHandler.java create mode 100644 dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/log/remote/AbsRemoteLogHandlerTest.java diff --git a/docs/docs/en/guide/remote-logging.md b/docs/docs/en/guide/remote-logging.md index a29dc065829c..7753fe4116a2 100644 --- a/docs/docs/en/guide/remote-logging.md +++ b/docs/docs/en/guide/remote-logging.md @@ -10,7 +10,7 @@ If you deploy DolphinScheduler in `Standalone` mode, you only need to configure ```properties # Whether to enable remote logging remote.logging.enable=false -# if remote.logging.enable = true, set the target of remote logging +# if remote.logging.enable = true, set the target of remote logging, currently support OSS, S3, GCS, ABS remote.logging.target=OSS # if remote.logging.enable = true, set the log base directory remote.logging.base.dir=logs @@ -66,12 +66,12 @@ remote.logging.google.cloud.storage.bucket.name= Configure `common.properties` as follows: ```properties -# abs container name, required if you set resource.storage.type=ABS -resource.azure.blob.storage.container.name= # abs account name, required if you set resource.storage.type=ABS -resource.azure.blob.storage.account.name= -# abs connection string, required if you set resource.storage.type=ABS -resource.azure.blob.storage.connection.string= +remote.logging.abs.account.name= +# abs account key, required if you set resource.storage.type=ABS +remote.logging.abs.account.key= +# abs container name, required if you set resource.storage.type=ABS +remote.logging.abs.container.name= ``` ### Notice diff --git a/docs/docs/zh/guide/remote-logging.md b/docs/docs/zh/guide/remote-logging.md index 7321badb1aa3..0e45353636b4 100644 --- a/docs/docs/zh/guide/remote-logging.md +++ b/docs/docs/zh/guide/remote-logging.md @@ -10,7 +10,7 @@ Apache DolphinScheduler支持将任务日志传输到远端存储上。当配置 ```properties # 是否开启远程日志存储 remote.logging.enable=true -# 任务日志写入的远端存储,目前支持OSS, S3, GCS +# 任务日志写入的远端存储,目前支持OSS, S3, GCS, ABS remote.logging.target=OSS # 任务日志在远端存储上的目录 remote.logging.base.dir=logs @@ -66,12 +66,12 @@ remote.logging.google.cloud.storage.bucket.name= 配置`common.propertis`如下: ```properties -# abs container name, required if you set resource.storage.type=ABS -resource.azure.blob.storage.container.name= # abs account name, required if you set resource.storage.type=ABS -resource.azure.blob.storage.account.name= -# abs connection string, required if you set resource.storage.type=ABS -resource.azure.blob.storage.connection.string= +remote.logging.abs.account.name= +# abs account key, required if you set resource.storage.type=ABS +remote.logging.abs.account.key= +# abs container name, required if you set resource.storage.type=ABS +remote.logging.abs.container.name= ``` ### 注意事项 diff --git a/dolphinscheduler-common/pom.xml b/dolphinscheduler-common/pom.xml index b03fdd74832b..eda9a72e307f 100644 --- a/dolphinscheduler-common/pom.xml +++ b/dolphinscheduler-common/pom.xml @@ -98,6 +98,11 @@ esdk-obs-java-bundle + + com.azure + azure-storage-blob + + com.github.oshi oshi-core diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java index 755663621671..054a9410d5f1 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java @@ -725,6 +725,13 @@ private Constants() { public static final String REMOTE_LOGGING_GCS_BUCKET_NAME = "remote.logging.google.cloud.storage.bucket.name"; + /** + * remote logging for ABS + */ + public static final String REMOTE_LOGGING_ABS_ACCOUNT_NAME = "remote.logging.abs.account.name"; + public static final String REMOTE_LOGGING_ABS_ACCOUNT_KEY = "remote.logging.abs.account.key"; + public static final String REMOTE_LOGGING_ABS_CONTAINER_NAME = "remote.logging.abs.container.name"; + /** * data quality */ diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/AbsRemoteLogHandler.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/AbsRemoteLogHandler.java new file mode 100644 index 000000000000..c0df3f6287c7 --- /dev/null +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/AbsRemoteLogHandler.java @@ -0,0 +1,137 @@ +/* + * 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.common.log.remote; + +import org.apache.dolphinscheduler.common.constants.Constants; +import org.apache.dolphinscheduler.common.utils.PropertyUtils; + +import org.apache.commons.lang3.StringUtils; + +import java.io.Closeable; +import java.io.FileOutputStream; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.specialized.BlobInputStream; +import com.azure.storage.common.StorageSharedKeyCredential; + +@Slf4j +public class AbsRemoteLogHandler implements RemoteLogHandler, Closeable { + + private String accountName; + + private String accountKey; + + private String containerName; + + private BlobContainerClient blobContainerClient; + + private static AbsRemoteLogHandler instance; + + private AbsRemoteLogHandler() { + accountName = readAccountName(); + accountKey = readAccountKey(); + containerName = readContainerName(); + blobContainerClient = buildBlobContainerClient(); + } + + public static synchronized AbsRemoteLogHandler getInstance() { + if (instance == null) { + instance = new AbsRemoteLogHandler(); + } + + return instance; + } + + protected BlobContainerClient buildBlobContainerClient() { + + BlobServiceClient serviceClient = new BlobServiceClientBuilder() + .endpoint(String.format("https://%s.blob.core.windows.net/", accountName)) + .credential(new StorageSharedKeyCredential(accountName, accountKey)) + .buildClient(); + + if (StringUtils.isBlank(containerName)) { + throw new IllegalArgumentException("remote.logging.abs.container.name is blank"); + } + + try { + this.blobContainerClient = serviceClient.getBlobContainerClient(containerName); + } catch (Exception ex) { + throw new IllegalArgumentException( + "containerName: " + containerName + " is not exists, you need to create them by yourself"); + } + + log.info("containerName: {} has been found.", containerName); + + return blobContainerClient; + } + + @Override + public void close() throws IOException { + // no need to close blobContainerClient + } + + @Override + public void sendRemoteLog(String logPath) { + String objectName = RemoteLogUtils.getObjectNameFromLogPath(logPath); + + try { + log.info("send remote log {} to Azure Blob {}", logPath, objectName); + blobContainerClient.getBlobClient(objectName).uploadFromFile(logPath); + } catch (Exception e) { + log.error("error while sending remote log {} to Azure Blob {}", logPath, objectName, e); + } + } + + @Override + public void getRemoteLog(String logPath) { + String objectName = RemoteLogUtils.getObjectNameFromLogPath(logPath); + + try { + log.info("get remote log on Azure Blob {} to {}", objectName, logPath); + + try ( + BlobInputStream bis = blobContainerClient.getBlobClient(objectName).openInputStream(); + FileOutputStream fos = new FileOutputStream(logPath)) { + byte[] readBuf = new byte[1024]; + int readLen = 0; + while ((readLen = bis.read(readBuf)) > 0) { + fos.write(readBuf, 0, readLen); + } + } + } catch (Exception e) { + log.error("error while getting remote log on Azure Blob {} to {}", objectName, logPath, e); + } + } + + protected String readAccountName() { + return PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_ACCOUNT_NAME); + } + + protected String readAccountKey() { + return PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_ACCOUNT_KEY); + } + + protected String readContainerName() { + return PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_CONTAINER_NAME); + } +} diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/RemoteLogHandlerFactory.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/RemoteLogHandlerFactory.java index 73ab41a134a8..ac75a23f2d30 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/RemoteLogHandlerFactory.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/RemoteLogHandlerFactory.java @@ -39,6 +39,8 @@ public RemoteLogHandler getRemoteLogHandler() { return S3RemoteLogHandler.getInstance(); } else if ("GCS".equals(target)) { return GcsRemoteLogHandler.getInstance(); + } else if ("ABS".equals(target)) { + return AbsRemoteLogHandler.getInstance(); } log.error("No suitable remote logging target for {}", target); diff --git a/dolphinscheduler-common/src/main/resources/common.properties b/dolphinscheduler-common/src/main/resources/common.properties index 669d3dfef348..fdb553b4bcb6 100644 --- a/dolphinscheduler-common/src/main/resources/common.properties +++ b/dolphinscheduler-common/src/main/resources/common.properties @@ -202,4 +202,9 @@ remote.logging.s3.region= remote.logging.google.cloud.storage.credential=/path/to/credential # gcs bucket name, required if you set remote.logging.target=GCS remote.logging.google.cloud.storage.bucket.name= - +# abs account name, required if you set resource.storage.type=ABS +remote.logging.abs.account.name= +# abs account key, required if you set resource.storage.type=ABS +remote.logging.abs.account.key= +# abs container name, required if you set resource.storage.type=ABS +remote.logging.abs.container.name= diff --git a/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/log/remote/AbsRemoteLogHandlerTest.java b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/log/remote/AbsRemoteLogHandlerTest.java new file mode 100644 index 000000000000..bc18f952a9ed --- /dev/null +++ b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/log/remote/AbsRemoteLogHandlerTest.java @@ -0,0 +1,144 @@ +/* + * 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.common.log.remote; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import org.apache.dolphinscheduler.common.constants.Constants; +import org.apache.dolphinscheduler.common.utils.LogUtils; +import org.apache.dolphinscheduler.common.utils.PropertyUtils; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.common.StorageSharedKeyCredential; + +@Slf4j +@ExtendWith(MockitoExtension.class) +public class AbsRemoteLogHandlerTest { + + @Mock + BlobServiceClient blobServiceClient; + + @Mock + BlobContainerClient blobContainerClient; + + @Mock + BlobClient blobClient; + + @Test + public void testAbsRemoteLogHandlerContainerNameBlack() { + try ( + MockedStatic propertyUtilsMockedStatic = Mockito.mockStatic(PropertyUtils.class); + MockedStatic remoteLogUtilsMockedStatic = Mockito.mockStatic(LogUtils.class)) { + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_ACCOUNT_NAME)) + .thenReturn("account_name"); + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_ACCOUNT_KEY)) + .thenReturn("account_key"); + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_CONTAINER_NAME)) + .thenReturn(""); + remoteLogUtilsMockedStatic.when(LogUtils::getLocalLogBaseDir).thenReturn("logs"); + + IllegalArgumentException thrown = Assertions.assertThrows(IllegalArgumentException.class, () -> { + AbsRemoteLogHandler.getInstance(); + }); + Assertions.assertEquals("remote.logging.abs.container.name is blank", thrown.getMessage()); + } + } + + @Test + public void testAbsRemoteLogHandlerContainerNotExists() { + try ( + MockedStatic propertyUtilsMockedStatic = Mockito.mockStatic(PropertyUtils.class); + MockedStatic remoteLogUtilsMockedStatic = Mockito.mockStatic(LogUtils.class); + MockedConstruction k8sClientWrapperMockedConstruction = + Mockito.mockConstruction(BlobServiceClientBuilder.class, (mock, context) -> { + when(mock.endpoint(any(String.class))).thenReturn(mock); + when(mock.credential(any(StorageSharedKeyCredential.class))).thenReturn(mock); + when(mock.buildClient()) + .thenReturn(blobServiceClient); + })) { + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_ACCOUNT_NAME)) + .thenReturn("account_name"); + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_ACCOUNT_KEY)) + .thenReturn("account_key"); + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_CONTAINER_NAME)) + .thenReturn("container_name"); + remoteLogUtilsMockedStatic.when(LogUtils::getLocalLogBaseDir).thenReturn("logs"); + + when(blobServiceClient.getBlobContainerClient(any(String.class))).thenThrow( + new NullPointerException("container not exists")); + IllegalArgumentException thrown = Assertions.assertThrows(IllegalArgumentException.class, () -> { + AbsRemoteLogHandler.getInstance(); + }); + Assertions.assertEquals("containerName: container_name is not exists, you need to create them by yourself", + thrown.getMessage()); + } + } + + @Test + public void testAbsRemoteLogHandler() { + + try ( + MockedStatic propertyUtilsMockedStatic = Mockito.mockStatic(PropertyUtils.class); + MockedStatic remoteLogUtilsMockedStatic = Mockito.mockStatic(LogUtils.class); + MockedConstruction blobServiceClientBuilderMockedConstruction = + Mockito.mockConstruction(BlobServiceClientBuilder.class, (mock, context) -> { + when(mock.endpoint(any(String.class))).thenReturn(mock); + when(mock.credential(any(StorageSharedKeyCredential.class))).thenReturn(mock); + when(mock.buildClient()) + .thenReturn(blobServiceClient); + }); + MockedStatic remoteLogUtilsMockedStatic1 = Mockito.mockStatic(RemoteLogUtils.class)) { + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_ACCOUNT_NAME)) + .thenReturn("account_name"); + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_ACCOUNT_KEY)) + .thenReturn("account_key"); + propertyUtilsMockedStatic.when(() -> PropertyUtils.getString(Constants.REMOTE_LOGGING_ABS_CONTAINER_NAME)) + .thenReturn("container_name"); + remoteLogUtilsMockedStatic.when(LogUtils::getLocalLogBaseDir).thenReturn("logs"); + String logPath = "logpath"; + String objectName = "objectname"; + remoteLogUtilsMockedStatic1.when(() -> RemoteLogUtils.getObjectNameFromLogPath(logPath)) + .thenReturn(objectName); + + when(blobServiceClient.getBlobContainerClient(any(String.class))).thenReturn(blobContainerClient); + when(blobContainerClient.getBlobClient(objectName)).thenReturn(blobClient); + + AbsRemoteLogHandler absRemoteLogHandler = AbsRemoteLogHandler.getInstance(); + Assertions.assertNotNull(absRemoteLogHandler); + + absRemoteLogHandler.sendRemoteLog(logPath); + Mockito.verify(blobClient, times(1)).uploadFromFile(logPath); + } + } +} From 8acc69794275149a359a711359873b39781f58b7 Mon Sep 17 00:00:00 2001 From: caishunfeng Date: Mon, 1 Apr 2024 22:49:59 +0800 Subject: [PATCH 006/165] [Improvement] add resource full name check (#15786) * [Improvement] add resource full name check --- .../dolphinscheduler/api/enums/Status.java | 2 + .../api/service/ResourcesService.java | 6 +- .../service/impl/ResourcesServiceImpl.java | 20 +- .../api/service/ResourcesServiceTest.java | 333 ++++++++---------- 4 files changed, 168 insertions(+), 193 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java index 113ccb6bd1dd..9dc12d9ba056 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java @@ -323,6 +323,8 @@ public enum Status { REMOVE_TASK_INSTANCE_CACHE_ERROR(20019, "remove task instance cache error", "删除任务实例缓存错误"), + ILLEGAL_RESOURCE_PATH(20020, "Resource file [{0}] is illegal", "非法的资源路径[{0}]"), + USER_NO_OPERATION_PERM(30001, "user has no operation privilege", "当前用户没有操作权限"), USER_NO_OPERATION_PROJECT_PERM(30002, "user {0} is not has project {1} permission", "当前用户[{0}]没有[{1}]项目的操作权限"), USER_NO_WRITE_PROJECT_PERM(30003, "user [{0}] does not have write permission for project [{1}]", diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ResourcesService.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ResourcesService.java index 24d1ba8727dd..54023baad9e4 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ResourcesService.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ResourcesService.java @@ -194,13 +194,13 @@ Result updateResourceContent(User loginUser, String fullName, String ten org.springframework.core.io.Resource downloadResource(User loginUser, String fullName) throws IOException; /** - * Get resource by given resource type and full name. + * Get resource by given resource type and file name. * Useful in Python API create task which need processDefinition information. * * @param userName user who query resource - * @param fullName full name of the resource + * @param fileName file name of the resource */ - StorageEntity queryFileStatus(String userName, String fullName) throws Exception; + StorageEntity queryFileStatus(String userName, String fileName) throws Exception; /** * delete DATA_TRANSFER data in resource center diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ResourcesServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ResourcesServiceImpl.java index e6341f021d20..6a15da17a804 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ResourcesServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ResourcesServiceImpl.java @@ -126,6 +126,7 @@ public Result createDirectory(User loginUser, String name, ResourceType } String tenantCode = getTenantCode(user); + checkFullName(tenantCode, currentDir); String userResRootPath = ResourceType.UDF.equals(type) ? storageOperate.getUdfDir(tenantCode) : storageOperate.getResDir(tenantCode); @@ -171,6 +172,7 @@ public Result uploadResource(User loginUser, String name, ResourceType t } String tenantCode = getTenantCode(user); + checkFullName(tenantCode, currentDir); result = verifyFile(name, type, file); if (!result.getCode().equals(Status.SUCCESS.getCode())) { @@ -257,6 +259,7 @@ public Result updateResource(User loginUser, String resourceFullName, St } String tenantCode = getTenantCode(user); + checkFullName(tenantCode, resourceFullName); if (!isUserTenantValid(isAdmin(loginUser), tenantCode, resTenantCode)) { log.error("current user does not have permission"); @@ -264,7 +267,7 @@ public Result updateResource(User loginUser, String resourceFullName, St return result; } - String defaultPath = storageOperate.getResDir(tenantCode); + String defaultPath = storageOperate.getDir(type, tenantCode); StorageEntity resource; try { @@ -949,6 +952,7 @@ public Result createResourceFile(User loginUser, ResourceType type, Stri } String tenantCode = getTenantCode(user); + checkFullName(tenantCode, currentDir); if (FileUtils.directoryTraversal(fileName)) { log.warn("File name verify failed, fileName:{}.", RegexUtils.escapeNRT(fileName)); @@ -1280,9 +1284,19 @@ private String getTenantCode(User user) { } private void checkFullName(String userTenantCode, String fullName) { + if (StringUtils.isEmpty(fullName)) { + return; + } + if (FOLDER_SEPARATOR.equalsIgnoreCase(fullName)) { + return; + } + // Avoid returning to the parent directory + if (fullName.contains("../")) { + throw new ServiceException(Status.ILLEGAL_RESOURCE_PATH, fullName); + } String baseDir = storageOperate.getDir(ResourceType.ALL, userTenantCode); - if (StringUtils.isNotBlank(fullName) && !StringUtils.startsWith(fullName, baseDir)) { - throw new ServiceException("Resource file: " + fullName + " is illegal"); + if (!StringUtils.startsWith(fullName, baseDir)) { + throw new ServiceException(Status.ILLEGAL_RESOURCE_PATH, fullName); } } } diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ResourcesServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ResourcesServiceTest.java index 0679b8892dff..6e94a258610c 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ResourcesServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ResourcesServiceTest.java @@ -18,7 +18,6 @@ package org.apache.dolphinscheduler.api.service; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -70,8 +69,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.mock.web.MockMultipartFile; import com.google.common.io.Files; @@ -83,9 +80,10 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class ResourcesServiceTest { - private static final Logger logger = LoggerFactory.getLogger(ResourcesServiceTest.class); - + private static final String basePath = "/dolphinscheduler"; private static final String tenantCode = "123"; + private static final String tenantFileResourceDir = "/dolphinscheduler/123/resources/"; + private static final String tenantUdfResourceDir = "/dolphinscheduler/123/udfs/"; @InjectMocks private ResourcesServiceImpl resourcesService; @@ -153,18 +151,30 @@ public void testCreateResource() { // CURRENT_LOGIN_USER_TENANT_NOT_EXIST when(userMapper.selectById(user.getId())).thenReturn(getUser()); when(tenantMapper.queryById(1)).thenReturn(null); - Assertions.assertThrows(ServiceException.class, + ServiceException serviceException = Assertions.assertThrows(ServiceException.class, () -> resourcesService.uploadResource(user, "ResourcesServiceTest", ResourceType.FILE, new MockMultipartFile("test.pdf", "test.pdf", "pdf", "test".getBytes()), "/")); + assertEquals(Status.CURRENT_LOGIN_USER_TENANT_NOT_EXIST.getMsg(), serviceException.getMessage()); + // set tenant for user user.setTenantId(1); when(tenantMapper.queryById(1)).thenReturn(getTenant()); + when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn(basePath); + + // ILLEGAL_RESOURCE_FILE + String illegal_path = "/dolphinscheduler/123/../"; + serviceException = Assertions.assertThrows(ServiceException.class, + () -> { + MockMultipartFile mockMultipartFile = new MockMultipartFile("test.pdf", "".getBytes()); + resourcesService.uploadResource(user, "ResourcesServiceTest", ResourceType.FILE, + mockMultipartFile, illegal_path); + }); + assertEquals(new ServiceException(Status.ILLEGAL_RESOURCE_PATH, illegal_path), serviceException); // RESOURCE_FILE_IS_EMPTY MockMultipartFile mockMultipartFile = new MockMultipartFile("test.pdf", "".getBytes()); Result result = resourcesService.uploadResource(user, "ResourcesServiceTest", ResourceType.FILE, - mockMultipartFile, "/"); - logger.info(result.toString()); + mockMultipartFile, tenantFileResourceDir); assertEquals(Status.RESOURCE_FILE_IS_EMPTY.getMsg(), result.getMsg()); // RESOURCE_SUFFIX_FORBID_CHANGE @@ -172,8 +182,7 @@ public void testCreateResource() { when(Files.getFileExtension("test.pdf")).thenReturn("pdf"); when(Files.getFileExtension("ResourcesServiceTest.jar")).thenReturn("jar"); result = resourcesService.uploadResource(user, "ResourcesServiceTest.jar", ResourceType.FILE, mockMultipartFile, - "/"); - logger.info(result.toString()); + tenantFileResourceDir); assertEquals(Status.RESOURCE_SUFFIX_FORBID_CHANGE.getMsg(), result.getMsg()); // UDF_RESOURCE_SUFFIX_NOT_JAR @@ -181,45 +190,42 @@ public void testCreateResource() { new MockMultipartFile("ResourcesServiceTest.pdf", "ResourcesServiceTest.pdf", "pdf", "test".getBytes()); when(Files.getFileExtension("ResourcesServiceTest.pdf")).thenReturn("pdf"); result = resourcesService.uploadResource(user, "ResourcesServiceTest.pdf", ResourceType.UDF, mockMultipartFile, - "/"); - logger.info(result.toString()); + tenantUdfResourceDir); assertEquals(Status.UDF_RESOURCE_SUFFIX_NOT_JAR.getMsg(), result.getMsg()); // FULL_FILE_NAME_TOO_LONG String tooLongFileName = getRandomStringWithLength(Constants.RESOURCE_FULL_NAME_MAX_LENGTH) + ".pdf"; mockMultipartFile = new MockMultipartFile(tooLongFileName, tooLongFileName, "pdf", "test".getBytes()); when(Files.getFileExtension(tooLongFileName)).thenReturn("pdf"); + // '/databasePath/tenantCode/RESOURCE/' - when(storageOperate.getResDir(tenantCode)).thenReturn("/dolphinscheduler/123/resources/"); - result = resourcesService.uploadResource(user, tooLongFileName, ResourceType.FILE, mockMultipartFile, "/"); - logger.info(result.toString()); + when(storageOperate.getResDir(tenantCode)).thenReturn(tenantFileResourceDir); + result = resourcesService.uploadResource(user, tooLongFileName, ResourceType.FILE, mockMultipartFile, + tenantFileResourceDir); assertEquals(Status.RESOURCE_FULL_NAME_TOO_LONG_ERROR.getMsg(), result.getMsg()); } @Test - public void testCreateDirecotry() { + public void testCreateDirecotry() throws IOException { User user = new User(); user.setId(1); user.setUserType(UserType.GENERAL_USER); + String fileName = "directoryTest"; // RESOURCE_EXIST user.setId(1); user.setTenantId(1); when(tenantMapper.queryById(1)).thenReturn(getTenant()); when(userMapper.selectById(user.getId())).thenReturn(getUser()); - when(storageOperate.getResDir(tenantCode)).thenReturn("/dolphinscheduler/123/resources/"); - try { - when(storageOperate.exists("/dolphinscheduler/123/resources/directoryTest")).thenReturn(true); - } catch (IOException e) { - logger.error(e.getMessage(), e); - } - Result result = resourcesService.createDirectory(user, "directoryTest", ResourceType.FILE, -1, "/"); - logger.info(result.toString()); + when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn(basePath); + when(storageOperate.getResDir(tenantCode)).thenReturn(tenantFileResourceDir); + when(storageOperate.exists(tenantFileResourceDir + fileName)).thenReturn(true); + Result result = resourcesService.createDirectory(user, fileName, ResourceType.FILE, -1, tenantFileResourceDir); assertEquals(Status.RESOURCE_EXIST.getMsg(), result.getMsg()); } @Test - public void testUpdateResource() { + public void testUpdateResource() throws Exception { User user = new User(); user.setId(1); user.setUserType(UserType.GENERAL_USER); @@ -227,7 +233,13 @@ public void testUpdateResource() { when(userMapper.selectById(user.getId())).thenReturn(getUser()); when(tenantMapper.queryById(1)).thenReturn(getTenant()); - when(storageOperate.getResDir(tenantCode)).thenReturn("/dolphinscheduler/123/resources/"); + when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn(basePath); + when(storageOperate.getResDir(tenantCode)).thenReturn(tenantFileResourceDir); + + // TENANT_NOT_EXIST + when(tenantMapper.queryById(Mockito.anyInt())).thenReturn(null); + Assertions.assertThrows(ServiceException.class, () -> resourcesService.updateResource(user, + "ResourcesServiceTest1.jar", "", "ResourcesServiceTest", ResourceType.UDF, null)); // USER_NO_OPERATION_PERM user.setUserType(UserType.GENERAL_USER); @@ -235,92 +247,58 @@ public void testUpdateResource() { Tenant tenantWNoPermission = new Tenant(); tenantWNoPermission.setTenantCode("321"); when(tenantMapper.queryById(1)).thenReturn(tenantWNoPermission); - Result result = resourcesService.updateResource(user, "/dolphinscheduler/123/resources/ResourcesServiceTest", - tenantCode, "ResourcesServiceTest", ResourceType.FILE, null); - logger.info(result.toString()); + when(storageOperate.getDir(ResourceType.ALL, "321")).thenReturn(basePath); + + String fileName = "ResourcesServiceTest"; + Result result = resourcesService.updateResource(user, tenantFileResourceDir + fileName, + tenantCode, fileName, ResourceType.FILE, null); assertEquals(Status.NO_CURRENT_OPERATING_PERMISSION.getMsg(), result.getMsg()); // SUCCESS when(tenantMapper.queryById(1)).thenReturn(getTenant()); - try { - when(storageOperate.exists(Mockito.any())).thenReturn(false); - } catch (IOException e) { - logger.error(e.getMessage(), e); - } - - try { - when(storageOperate.getFileStatus("/dolphinscheduler/123/resources/ResourcesServiceTest", - "/dolphinscheduler/123/resources/", tenantCode, ResourceType.FILE)) - .thenReturn(getStorageEntityResource()); - result = resourcesService.updateResource(user, "/dolphinscheduler/123/resources/ResourcesServiceTest", - tenantCode, "ResourcesServiceTest", ResourceType.FILE, null); - logger.info(result.toString()); - assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); - } catch (Exception e) { - logger.error(e.getMessage() + " Resource path: {}", "/dolphinscheduler/123/resources/ResourcesServiceTest", - e); - } + when(storageOperate.exists(Mockito.any())).thenReturn(false); + + when(storageOperate.getDir(ResourceType.FILE, tenantCode)).thenReturn(tenantFileResourceDir); + when(storageOperate.getFileStatus(tenantFileResourceDir + fileName, + tenantFileResourceDir, tenantCode, ResourceType.FILE)) + .thenReturn(getStorageEntityResource(fileName)); + result = resourcesService.updateResource(user, tenantFileResourceDir + fileName, + tenantCode, fileName, ResourceType.FILE, null); + assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); // Tests for udf resources. - // RESOURCE_EXIST - try { - when(storageOperate.exists("/dolphinscheduler/123/resources/ResourcesServiceTest2.jar")).thenReturn(true); - } catch (IOException e) { - logger.error("error occurred when checking resource: " - + "/dolphinscheduler/123/resources/ResourcesServiceTest2.jar"); - } - - try { - when(storageOperate.getFileStatus("/dolphinscheduler/123/resources/ResourcesServiceTest1.jar", - "/dolphinscheduler/123/resources/", tenantCode, ResourceType.UDF)) - .thenReturn(getStorageEntityUdfResource()); - } catch (Exception e) { - logger.error(e.getMessage() + " Resource path: {}", - "/dolphinscheduler/123/resources/ResourcesServiceTest1.jar", e); - } - result = resourcesService.updateResource(user, "/dolphinscheduler/123/resources/ResourcesServiceTest1.jar", - tenantCode, "ResourcesServiceTest2.jar", ResourceType.UDF, null); - logger.info(result.toString()); - assertEquals(Status.RESOURCE_EXIST.getMsg(), result.getMsg()); - - // TENANT_NOT_EXIST - when(tenantMapper.queryById(Mockito.anyInt())).thenReturn(null); - Assertions.assertThrows(ServiceException.class, () -> resourcesService.updateResource(user, - "ResourcesServiceTest1.jar", "", "ResourcesServiceTest", ResourceType.UDF, null)); - - // SUCCESS - when(tenantMapper.queryById(1)).thenReturn(getTenant()); - - result = resourcesService.updateResource(user, "/dolphinscheduler/123/resources/ResourcesServiceTest1.jar", - tenantCode, "ResourcesServiceTest1.jar", ResourceType.UDF, null); - logger.info(result.toString()); + fileName = "ResourcesServiceTest.jar"; + when(storageOperate.getDir(ResourceType.UDF, tenantCode)).thenReturn(tenantUdfResourceDir); + when(storageOperate.exists(tenantUdfResourceDir + fileName)).thenReturn(true); + when(storageOperate.getFileStatus(tenantUdfResourceDir + fileName, tenantUdfResourceDir, tenantCode, + ResourceType.UDF)) + .thenReturn(getStorageEntityUdfResource(fileName)); + result = resourcesService.updateResource(user, tenantUdfResourceDir + fileName, + tenantCode, fileName, ResourceType.UDF, null); assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); } @Test - public void testQueryResourceListPaging() { + public void testQueryResourceListPaging() throws Exception { User loginUser = new User(); loginUser.setId(1); loginUser.setTenantId(1); loginUser.setTenantCode("tenant1"); loginUser.setUserType(UserType.ADMIN_USER); - List mockResList = new ArrayList(); - mockResList.add(getStorageEntityResource()); - List mockUserList = new ArrayList(); + + String fileName = "ResourcesServiceTest"; + List mockResList = new ArrayList<>(); + mockResList.add(getStorageEntityResource(fileName)); + List mockUserList = new ArrayList<>(); mockUserList.add(getUser()); when(userMapper.selectList(null)).thenReturn(mockUserList); when(userMapper.selectById(getUser().getId())).thenReturn(getUser()); when(tenantMapper.queryById(getUser().getTenantId())).thenReturn(getTenant()); - when(storageOperate.getResDir(tenantCode)).thenReturn("/dolphinscheduler/123/resources/"); + when(storageOperate.getResDir(tenantCode)).thenReturn(tenantFileResourceDir); + when(storageOperate.listFilesStatus(tenantFileResourceDir, tenantFileResourceDir, + tenantCode, ResourceType.FILE)).thenReturn(mockResList); - try { - when(storageOperate.listFilesStatus("/dolphinscheduler/123/resources/", "/dolphinscheduler/123/resources/", - tenantCode, ResourceType.FILE)).thenReturn(mockResList); - } catch (Exception e) { - logger.error("QueryResourceListPaging Error"); - } Result result = resourcesService.queryResourceListPaging(loginUser, "", "", ResourceType.FILE, "Test", 1, 10); - logger.info(result.toString()); assertEquals(Status.SUCCESS.getCode(), (int) result.getCode()); PageInfo pageInfo = (PageInfo) result.getData(); Assertions.assertTrue(CollectionUtils.isNotEmpty(pageInfo.getTotalList())); @@ -330,29 +308,30 @@ public void testQueryResourceListPaging() { @Test public void testQueryResourceList() { User loginUser = getUser(); + String fileName = "ResourcesServiceTest"; when(userMapper.selectList(null)).thenReturn(Collections.singletonList(loginUser)); when(userMapper.selectById(loginUser.getId())).thenReturn(loginUser); when(tenantMapper.queryById(Mockito.anyInt())).thenReturn(getTenant()); - when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn("/dolphinscheduler"); - when(storageOperate.getDir(ResourceType.FILE, tenantCode)).thenReturn("/dolphinscheduler/123/resources/"); - when(storageOperate.getResDir(tenantCode)).thenReturn("/dolphinscheduler/123/resources/"); - when(storageOperate.listFilesStatusRecursively("/dolphinscheduler/123/resources/", - "/dolphinscheduler/123/resources/", tenantCode, ResourceType.FILE)) - .thenReturn(Collections.singletonList(getStorageEntityResource())); + when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn(basePath); + when(storageOperate.getDir(ResourceType.FILE, tenantCode)).thenReturn(tenantFileResourceDir); + when(storageOperate.getResDir(tenantCode)).thenReturn(tenantFileResourceDir); + when(storageOperate.listFilesStatusRecursively(tenantFileResourceDir, + tenantFileResourceDir, tenantCode, ResourceType.FILE)) + .thenReturn(Collections.singletonList(getStorageEntityResource(fileName))); Map result = - resourcesService.queryResourceList(loginUser, ResourceType.FILE, "/dolphinscheduler/123/resources/"); + resourcesService.queryResourceList(loginUser, ResourceType.FILE, tenantFileResourceDir); assertEquals(Status.SUCCESS, result.get(Constants.STATUS)); List resourceList = (List) result.get(Constants.DATA_LIST); Assertions.assertTrue(CollectionUtils.isNotEmpty(resourceList)); // test udf - when(storageOperate.getDir(ResourceType.UDF, tenantCode)).thenReturn("/dolphinscheduler/123/udfs/"); - when(storageOperate.getUdfDir(tenantCode)).thenReturn("/dolphinscheduler/123/udfs/"); - when(storageOperate.listFilesStatusRecursively("/dolphinscheduler/123/udfs/", "/dolphinscheduler/123/udfs/", - tenantCode, ResourceType.UDF)).thenReturn(Arrays.asList(getStorageEntityUdfResource())); + when(storageOperate.getDir(ResourceType.UDF, tenantCode)).thenReturn(tenantUdfResourceDir); + when(storageOperate.getUdfDir(tenantCode)).thenReturn(tenantUdfResourceDir); + when(storageOperate.listFilesStatusRecursively(tenantUdfResourceDir, tenantUdfResourceDir, + tenantCode, ResourceType.UDF)).thenReturn(Arrays.asList(getStorageEntityUdfResource("test.jar"))); loginUser.setUserType(UserType.GENERAL_USER); - result = resourcesService.queryResourceList(loginUser, ResourceType.UDF, "/dolphinscheduler/123/udfs/"); + result = resourcesService.queryResourceList(loginUser, ResourceType.UDF, tenantUdfResourceDir); assertEquals(Status.SUCCESS, result.get(Constants.STATUS)); resourceList = (List) result.get(Constants.DATA_LIST); Assertions.assertTrue(CollectionUtils.isNotEmpty(resourceList)); @@ -360,7 +339,6 @@ public void testQueryResourceList() { @Test public void testDelete() throws Exception { - User loginUser = new User(); loginUser.setId(0); loginUser.setUserType(UserType.GENERAL_USER); @@ -372,46 +350,40 @@ public void testDelete() throws Exception { Assertions.assertThrows(ServiceException.class, () -> resourcesService.delete(loginUser, "", "")); // RESOURCE_NOT_EXIST + String fileName = "ResourcesServiceTest"; when(tenantMapper.queryById(Mockito.anyInt())).thenReturn(getTenant()); - when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn("/dolphinscheduler"); - when(storageOperate.getResDir(getTenant().getTenantCode())).thenReturn("/dolphinscheduler/123/resources/"); - when(storageOperate.getFileStatus("/dolphinscheduler/123/resources/ResourcesServiceTest", - "/dolphinscheduler/123/resources/", tenantCode, null)) - .thenReturn(getStorageEntityResource()); - Result result = resourcesService.delete(loginUser, "/dolphinscheduler/123/resources/ResNotExist", tenantCode); + when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn(basePath); + when(storageOperate.getResDir(getTenant().getTenantCode())).thenReturn(tenantFileResourceDir); + when(storageOperate.getFileStatus(tenantFileResourceDir + fileName, tenantFileResourceDir, tenantCode, null)) + .thenReturn(getStorageEntityResource(fileName)); + Result result = resourcesService.delete(loginUser, tenantFileResourceDir + "ResNotExist", tenantCode); assertEquals(Status.RESOURCE_NOT_EXIST.getMsg(), result.getMsg()); // SUCCESS loginUser.setTenantId(1); - result = resourcesService.delete(loginUser, "/dolphinscheduler/123/resources/ResourcesServiceTest", tenantCode); + result = resourcesService.delete(loginUser, tenantFileResourceDir + fileName, tenantCode); assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); } @Test - public void testVerifyResourceName() { - + public void testVerifyResourceName() throws IOException { User user = new User(); user.setId(1); user.setUserType(UserType.GENERAL_USER); - try { - when(storageOperate.exists("/ResourcesServiceTest.jar")).thenReturn(true); - } catch (IOException e) { - logger.error("error occurred when checking resource: /ResourcesServiceTest.jar\""); - } - Result result = resourcesService.verifyResourceName("/ResourcesServiceTest.jar", ResourceType.FILE, user); - logger.info(result.toString()); + + String fileName = "ResourcesServiceTest"; + when(storageOperate.exists(tenantFileResourceDir + fileName)).thenReturn(true); + + Result result = resourcesService.verifyResourceName(tenantFileResourceDir + fileName, ResourceType.FILE, user); assertEquals(Status.RESOURCE_EXIST.getMsg(), result.getMsg()); // RESOURCE_FILE_EXIST - result = resourcesService.verifyResourceName("/ResourcesServiceTest.jar", ResourceType.FILE, user); - logger.info(result.toString()); + result = resourcesService.verifyResourceName(tenantFileResourceDir + fileName, ResourceType.FILE, user); Assertions.assertTrue(Status.RESOURCE_EXIST.getCode() == result.getCode()); // SUCCESS result = resourcesService.verifyResourceName("test2", ResourceType.FILE, user); - logger.info(result.toString()); assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); - } @Test @@ -441,8 +413,8 @@ public void testReadResource() throws IOException { // SUCCESS when(FileUtils.getResourceViewSuffixes()).thenReturn("jar,sh"); - when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn("/dolphinscheduler"); - when(storageOperate.getResDir(getTenant().getTenantCode())).thenReturn("/dolphinscheduler/123/resources/"); + when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn(basePath); + when(storageOperate.getResDir(getTenant().getTenantCode())).thenReturn(tenantFileResourceDir); when(userMapper.selectById(getUser().getId())).thenReturn(getUser()); when(tenantMapper.queryById(getUser().getTenantId())).thenReturn(getTenant()); when(storageOperate.exists(Mockito.any())).thenReturn(true); @@ -465,15 +437,16 @@ public void testCreateOrUpdateResource() throws Exception { exception.getMessage().contains("Not allow create or update resources without extension name")); // SUCCESS - when(storageOperate.getResDir(user.getTenantCode())).thenReturn("/dolphinscheduler/123/resources/"); + String fileName = "ResourcesServiceTest"; + when(storageOperate.getResDir(user.getTenantCode())).thenReturn(tenantFileResourceDir); when(FileUtils.getUploadFilename(Mockito.anyString(), Mockito.anyString())).thenReturn("test"); when(FileUtils.writeContent2File(Mockito.anyString(), Mockito.anyString())).thenReturn(true); when(storageOperate.getFileStatus(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.any())) - .thenReturn(getStorageEntityResource()); + .thenReturn(getStorageEntityResource(fileName)); StorageEntity storageEntity = resourcesService.createOrUpdateResource(user.getUserName(), "filename.txt", "my-content"); Assertions.assertNotNull(storageEntity); - assertEquals("/dolphinscheduler/123/resources/ResourcesServiceTest", storageEntity.getFullName()); + assertEquals(tenantFileResourceDir + fileName, storageEntity.getFullName()); } @Test @@ -482,33 +455,35 @@ public void testUpdateResourceContent() throws Exception { when(userMapper.selectById(getUser().getId())).thenReturn(getUser()); when(tenantMapper.queryById(1)).thenReturn(getTenant()); when(storageOperate.getResDir(Mockito.anyString())).thenReturn("/tmp"); + + String fileName = "ResourcesServiceTest.jar"; ServiceException serviceException = Assertions.assertThrows(ServiceException.class, () -> resourcesService.updateResourceContent(getUser(), - "/dolphinscheduler/123/resources/ResourcesServiceTest.jar", tenantCode, "content")); - assertTrue(serviceException.getMessage() - .contains("Resource file: /dolphinscheduler/123/resources/ResourcesServiceTest.jar is illegal")); + tenantFileResourceDir + fileName, tenantCode, "content")); + assertEquals(new ServiceException(Status.ILLEGAL_RESOURCE_PATH, tenantFileResourceDir + fileName), + serviceException); // RESOURCE_NOT_EXIST - when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn("/dolphinscheduler"); - when(storageOperate.getResDir(Mockito.anyString())).thenReturn("/dolphinscheduler/123/resources"); - when(storageOperate.getFileStatus("/dolphinscheduler/123/resources/ResourcesServiceTest.jar", "", tenantCode, - ResourceType.FILE)).thenReturn(null); - Result result = resourcesService.updateResourceContent(getUser(), - "/dolphinscheduler/123/resources/ResourcesServiceTest.jar", tenantCode, "content"); + when(storageOperate.getDir(ResourceType.ALL, tenantCode)).thenReturn(basePath); + when(storageOperate.getResDir(Mockito.anyString())).thenReturn(tenantFileResourceDir); + when(storageOperate.getFileStatus(tenantFileResourceDir + fileName, "", tenantCode, ResourceType.FILE)) + .thenReturn(null); + Result result = resourcesService.updateResourceContent(getUser(), tenantFileResourceDir + fileName, tenantCode, + "content"); assertEquals(Status.RESOURCE_NOT_EXIST.getMsg(), result.getMsg()); // RESOURCE_SUFFIX_NOT_SUPPORT_VIEW when(FileUtils.getResourceViewSuffixes()).thenReturn("class"); - when(storageOperate.getFileStatus("/dolphinscheduler/123/resources", "", tenantCode, ResourceType.FILE)) - .thenReturn(getStorageEntityResource()); + when(storageOperate.getFileStatus(tenantFileResourceDir, "", tenantCode, ResourceType.FILE)) + .thenReturn(getStorageEntityResource(fileName)); - result = resourcesService.updateResourceContent(getUser(), "/dolphinscheduler/123/resources", tenantCode, + result = resourcesService.updateResourceContent(getUser(), tenantFileResourceDir, tenantCode, "content"); assertEquals(Status.RESOURCE_SUFFIX_NOT_SUPPORT_VIEW.getMsg(), result.getMsg()); // USER_NOT_EXIST when(userMapper.selectById(getUser().getId())).thenReturn(null); - result = resourcesService.updateResourceContent(getUser(), "/dolphinscheduler/123/resources/123.class", + result = resourcesService.updateResourceContent(getUser(), tenantFileResourceDir + "123.class", tenantCode, "content"); Assertions.assertTrue(Status.USER_NOT_EXIST.getCode() == result.getCode()); @@ -517,11 +492,11 @@ public void testUpdateResourceContent() throws Exception { when(userMapper.selectById(getUser().getId())).thenReturn(getUser()); when(tenantMapper.queryById(1)).thenReturn(null); Assertions.assertThrows(ServiceException.class, () -> resourcesService.updateResourceContent(getUser(), - "/dolphinscheduler/123/resources/ResourcesServiceTest.jar", tenantCode, "content")); + tenantFileResourceDir + fileName, tenantCode, "content")); // SUCCESS - when(storageOperate.getFileStatus("/dolphinscheduler/123/resources/ResourcesServiceTest.jar", "", tenantCode, - ResourceType.FILE)).thenReturn(getStorageEntityResource()); + when(storageOperate.getFileStatus(tenantFileResourceDir + fileName, "", tenantCode, + ResourceType.FILE)).thenReturn(getStorageEntityResource(fileName)); when(Files.getFileExtension(Mockito.anyString())).thenReturn("jar"); when(FileUtils.getResourceViewSuffixes()).thenReturn("jar"); @@ -530,32 +505,25 @@ public void testUpdateResourceContent() throws Exception { when(FileUtils.getUploadFilename(Mockito.anyString(), Mockito.anyString())).thenReturn("test"); when(FileUtils.writeContent2File(Mockito.anyString(), Mockito.anyString())).thenReturn(true); result = resourcesService.updateResourceContent(getUser(), - "/dolphinscheduler/123/resources/ResourcesServiceTest.jar", tenantCode, "content"); - logger.info(result.toString()); + tenantFileResourceDir + fileName, tenantCode, "content"); assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); } @Test - public void testDownloadResource() { + public void testDownloadResource() throws IOException { when(tenantMapper.queryById(1)).thenReturn(getTenant()); when(userMapper.selectById(1)).thenReturn(getUser()); org.springframework.core.io.Resource resourceMock = Mockito.mock(org.springframework.core.io.Resource.class); Path path = Mockito.mock(Path.class); when(Paths.get(Mockito.any())).thenReturn(path); - try { - when(java.nio.file.Files.size(Mockito.any())).thenReturn(1L); - // resource null - org.springframework.core.io.Resource resource = resourcesService.downloadResource(getUser(), ""); - Assertions.assertNull(resource); - - when(org.apache.dolphinscheduler.api.utils.FileUtils.file2Resource(Mockito.any())).thenReturn(resourceMock); - resource = resourcesService.downloadResource(getUser(), ""); - Assertions.assertNotNull(resource); - } catch (Exception e) { - logger.error("DownloadResource error", e); - Assertions.assertTrue(false); - } - + when(java.nio.file.Files.size(Mockito.any())).thenReturn(1L); + // resource null + org.springframework.core.io.Resource resource = resourcesService.downloadResource(getUser(), ""); + Assertions.assertNull(resource); + + when(org.apache.dolphinscheduler.api.utils.FileUtils.file2Resource(Mockito.any())).thenReturn(resourceMock); + resource = resourcesService.downloadResource(getUser(), ""); + Assertions.assertNotNull(resource); } @Test @@ -605,30 +573,21 @@ public void testDeleteDataTransferData() throws Exception { } @Test - public void testCatFile() { + public void testCatFile() throws IOException { // SUCCESS - try { - List list = storageOperate.vimFile(Mockito.any(), Mockito.anyString(), eq(1), eq(10)); - Assertions.assertNotNull(list); - - } catch (IOException e) { - logger.error("hadoop error", e); - } + List list = storageOperate.vimFile(Mockito.any(), Mockito.anyString(), eq(1), eq(10)); + Assertions.assertNotNull(list); } @Test - void testQueryBaseDir() { + void testQueryBaseDir() throws Exception { User user = getUser(); + String fileName = "ResourcesServiceTest.jar"; when(userMapper.selectById(user.getId())).thenReturn(getUser()); when(tenantMapper.queryById(user.getTenantId())).thenReturn(getTenant()); - when(storageOperate.getDir(ResourceType.FILE, tenantCode)).thenReturn("/dolphinscheduler/123/resources/"); - try { - when(storageOperate.getFileStatus(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), - Mockito.any())).thenReturn(getStorageEntityResource()); - } catch (Exception e) { - logger.error(e.getMessage() + " Resource path: {}", "/dolphinscheduler/123/resources/ResourcesServiceTest", - e); - } + when(storageOperate.getDir(ResourceType.FILE, tenantCode)).thenReturn(tenantFileResourceDir); + when(storageOperate.getFileStatus(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), + Mockito.any())).thenReturn(getStorageEntityResource(fileName)); Result result = resourcesService.queryResourceBaseDir(user, ResourceType.FILE); assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); } @@ -648,25 +607,25 @@ private User getUser() { return user; } - private StorageEntity getStorageEntityResource() { + private StorageEntity getStorageEntityResource(String fileName) { StorageEntity entity = new StorageEntity(); - entity.setAlias("ResourcesServiceTest"); - entity.setFileName("ResourcesServiceTest"); + entity.setAlias(fileName); + entity.setFileName(fileName); entity.setDirectory(false); entity.setUserName(tenantCode); entity.setType(ResourceType.FILE); - entity.setFullName("/dolphinscheduler/123/resources/ResourcesServiceTest"); + entity.setFullName(tenantFileResourceDir + fileName); return entity; } - private StorageEntity getStorageEntityUdfResource() { + private StorageEntity getStorageEntityUdfResource(String fileName) { StorageEntity entity = new StorageEntity(); - entity.setAlias("ResourcesServiceTest1.jar"); - entity.setFileName("ResourcesServiceTest1.jar"); + entity.setAlias(fileName); + entity.setFileName(fileName); entity.setDirectory(false); entity.setUserName(tenantCode); entity.setType(ResourceType.UDF); - entity.setFullName("/dolphinscheduler/123/resources/ResourcesServiceTest1.jar"); + entity.setFullName(tenantUdfResourceDir + fileName); return entity; } From 920ac154cb9c0c8f33670af7c67387c0e30f6dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=8F=AF=E8=80=90?= <46134044+sdhzwc@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:42:44 +0800 Subject: [PATCH 007/165] [Improvement-15744][parameter] project parameter add update time and update user id (#15745) * project parameter add update time and update user id * project parameter add comment operator user id and UT * project parameter add ui --- .../service/impl/ProjectParameterServiceImpl.java | 2 ++ .../api/service/ProjectParameterServiceTest.java | 3 +++ .../dao/entity/ProjectParameter.java | 8 ++++++++ .../dao/mapper/ProjectParameterMapper.xml | 14 +++++++++----- .../src/main/resources/sql/dolphinscheduler_h2.sql | 1 + .../main/resources/sql/dolphinscheduler_mysql.sql | 1 + .../resources/sql/dolphinscheduler_postgresql.sql | 1 + .../3.2.2_schema/mysql/dolphinscheduler_ddl.sql | 1 + .../postgresql/dolphinscheduler_ddl.sql | 4 +++- dolphinscheduler-ui/src/locales/en_US/project.ts | 2 ++ dolphinscheduler-ui/src/locales/zh_CN/project.ts | 2 ++ .../src/views/projects/parameter/use-table.ts | 10 ++++++++++ 12 files changed, 43 insertions(+), 6 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java index 6e66f6286e9c..e30375e80945 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java @@ -155,6 +155,8 @@ public Result updateProjectParameter(User loginUser, long projectCode, long code projectParameter.setParamName(projectParameterName); projectParameter.setParamValue(projectParameterValue); + projectParameter.setUpdateTime(new Date()); + projectParameter.setOperator(loginUser.getId()); if (projectParameterMapper.updateById(projectParameter) > 0) { log.info("Project parameter is updated and id is :{}", projectParameter.getId()); diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectParameterServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectParameterServiceTest.java index 4ab22bda21d6..7a3fb1b68d32 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectParameterServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectParameterServiceTest.java @@ -98,6 +98,9 @@ public void testUpdateProjectParameter() { Mockito.when(projectParameterMapper.updateById(Mockito.any())).thenReturn(1); result = projectParameterService.updateProjectParameter(loginUser, projectCode, 1, "key1", "value"); Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + ProjectParameter projectParameter = (ProjectParameter) result.getData(); + Assertions.assertNotNull(projectParameter.getOperator()); + Assertions.assertNotNull(projectParameter.getUpdateTime()); } @Test diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ProjectParameter.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ProjectParameter.java index fbeeb387f1ba..03e9140145c5 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ProjectParameter.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ProjectParameter.java @@ -42,6 +42,8 @@ public class ProjectParameter { @TableField("user_id") private Integer userId; + private Integer operator; + private long code; @TableField("project_code") @@ -56,4 +58,10 @@ public class ProjectParameter { private Date createTime; private Date updateTime; + + @TableField(exist = false) + private String createUser; + + @TableField(exist = false) + private String modifyUser; } diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ProjectParameterMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ProjectParameterMapper.xml index 159c263ea44d..5b22d40a81a7 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ProjectParameterMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ProjectParameterMapper.xml @@ -19,7 +19,7 @@ - id, param_name, param_value, code, project_code, user_id, create_time, update_time + id, param_name, param_value, code, project_code, user_id, operator, create_time, update_time select - - from t_ds_project_parameter + pp.id, param_name, param_value, code, project_code, user_id, operator, pp.create_time, pp.update_time, + u.user_name as create_user, + u2.user_name as modify_user + from t_ds_project_parameter pp + left join t_ds_user u on pp.user_id = u.id + left join t_ds_user u2 on pp.operator = u2.id where project_code = #{projectCode} - and id in + and pp.id in #{id} @@ -65,7 +69,7 @@ OR param_value LIKE concat('%', #{searchName}, '%') ) - order by update_time desc + order by pp.update_time desc diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ProcessInstanceMapperTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ProcessInstanceMapperTest.java index 39b8d04e4d12..0aae0dce2be3 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ProcessInstanceMapperTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ProcessInstanceMapperTest.java @@ -263,7 +263,8 @@ public void testQueryLastSchedulerProcess() { processInstanceMapper.updateById(processInstance); ProcessInstance processInstance1 = - processInstanceMapper.queryLastSchedulerProcess(processInstance.getProcessDefinitionCode(), null, null, + processInstanceMapper.queryLastSchedulerProcess(processInstance.getProcessDefinitionCode(), 0L, null, + null, processInstance.getTestFlag()); Assertions.assertNotEquals(null, processInstance1); processInstanceMapper.deleteById(processInstance.getId()); diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/DependentExecute.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/DependentExecute.java index 15ab34de34e8..28f9fd682bfe 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/DependentExecute.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/utils/DependentExecute.java @@ -320,7 +320,7 @@ private ProcessInstance findLastProcessInterval(Long definitionCode, Long taskCo int testFlag) { ProcessInstance lastSchedulerProcess = - processInstanceDao.queryLastSchedulerProcessInterval(definitionCode, dateInterval, testFlag); + processInstanceDao.queryLastSchedulerProcessInterval(definitionCode, taskCode, dateInterval, testFlag); ProcessInstance lastManualProcess = processInstanceDao.queryLastManualProcessInterval(definitionCode, taskCode, dateInterval, testFlag); From 5d8808dda40a5718df14a3ff703f07dfa253203c Mon Sep 17 00:00:00 2001 From: songwenyong <119404633+songwenyong@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:31:03 +0800 Subject: [PATCH 010/165] [Fix-15760][datasource-plugin] fix sql task split error (#15760) (#15794) * Fix the bug in SQL splitting by completing the task in two steps: 1. removeComment 2. split * Add a unit test for Hive SQL splitting. --- .../AbstractDataSourceProcessor.java | 3 ++- .../param/ClickHouseDataSourceProcessor.java | 3 ++- .../param/DamengDataSourceProcessor.java | 3 ++- .../db2/param/Db2DataSourceProcessor.java | 3 ++- .../hive/param/HiveDataSourceProcessor.java | 3 ++- .../param/HiveDataSourceProcessorTest.java | 20 +++++++++++++++++++ .../mysql/param/MySQLDataSourceProcessor.java | 3 ++- .../param/OceanBaseDataSourceProcessor.java | 3 ++- .../param/PostgreSQLDataSourceProcessor.java | 3 ++- .../param/SQLServerDataSourceProcessor.java | 3 ++- .../trino/param/TrinoDataSourceProcessor.java | 3 ++- 11 files changed, 40 insertions(+), 10 deletions(-) diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/AbstractDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/AbstractDataSourceProcessor.java index db357e9d5ce2..98222a2c5add 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/AbstractDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/AbstractDataSourceProcessor.java @@ -134,6 +134,7 @@ public boolean checkDataSourceConnectivity(ConnectionParam connectionParam) { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.other); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.other); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.other); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-clickhouse/src/main/java/org/apache/dolphinscheduler/plugin/datasource/clickhouse/param/ClickHouseDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-clickhouse/src/main/java/org/apache/dolphinscheduler/plugin/datasource/clickhouse/param/ClickHouseDataSourceProcessor.java index a613806460ea..81a4415f5d15 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-clickhouse/src/main/java/org/apache/dolphinscheduler/plugin/datasource/clickhouse/param/ClickHouseDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-clickhouse/src/main/java/org/apache/dolphinscheduler/plugin/datasource/clickhouse/param/ClickHouseDataSourceProcessor.java @@ -129,7 +129,8 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.clickhouse); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.clickhouse); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.clickhouse); } private String transformOther(Map otherMap) { diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-dameng/src/main/java/org/apache/dolphinscheduler/plugin/datasource/dameng/param/DamengDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-dameng/src/main/java/org/apache/dolphinscheduler/plugin/datasource/dameng/param/DamengDataSourceProcessor.java index 1af61facd37c..bc31bdcd492b 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-dameng/src/main/java/org/apache/dolphinscheduler/plugin/datasource/dameng/param/DamengDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-dameng/src/main/java/org/apache/dolphinscheduler/plugin/datasource/dameng/param/DamengDataSourceProcessor.java @@ -135,7 +135,8 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.dm); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.dm); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.dm); } private String transformOther(Map paramMap) { diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-db2/src/main/java/org/apache/dolphinscheduler/plugin/datasource/db2/param/Db2DataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-db2/src/main/java/org/apache/dolphinscheduler/plugin/datasource/db2/param/Db2DataSourceProcessor.java index 2d67d9184373..1d7c448355e2 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-db2/src/main/java/org/apache/dolphinscheduler/plugin/datasource/db2/param/Db2DataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-db2/src/main/java/org/apache/dolphinscheduler/plugin/datasource/db2/param/Db2DataSourceProcessor.java @@ -129,7 +129,8 @@ public String getValidationQuery() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.db2); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.db2); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.db2); } private String transformOther(Map otherMap) { diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/param/HiveDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/param/HiveDataSourceProcessor.java index 36330c17c860..09d9f4b9630d 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/param/HiveDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/param/HiveDataSourceProcessor.java @@ -152,7 +152,8 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.hive); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.hive); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.hive); } private String transformOther(Map otherMap) { diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/test/java/org/apache/dolphinscheduler/plugin/datasource/hive/param/HiveDataSourceProcessorTest.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/test/java/org/apache/dolphinscheduler/plugin/datasource/hive/param/HiveDataSourceProcessorTest.java index 1da0cbadda94..ef5e71729cc3 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/test/java/org/apache/dolphinscheduler/plugin/datasource/hive/param/HiveDataSourceProcessorTest.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/test/java/org/apache/dolphinscheduler/plugin/datasource/hive/param/HiveDataSourceProcessorTest.java @@ -23,6 +23,7 @@ import org.apache.dolphinscheduler.spi.enums.DbType; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; @@ -94,4 +95,23 @@ public void testGetValidationQuery() { Assertions.assertEquals(DataSourceConstants.HIVE_VALIDATION_QUERY, hiveDatasourceProcessor.getValidationQuery()); } + + @Test + void splitAndRemoveComment() { + String sql = "create table if not exists test_ods.tb_test(\n" + + " `id` bigint COMMENT 'id', -- auto increment\n" + + " `user_name` string COMMENT 'username',\n" + + " `birthday` string COMMENT 'birthday',\n" + + " `gender` int COMMENT '1 male 2 female'\n" + + ") COMMENT 'user information table' PARTITIONED BY (`date_id` string);\n" + + "\n" + + "-- insert\n" + + "insert\n" + + " overwrite table test_ods.tb_test partition(date_id = '2024-03-28') -- partition\n" + + "values\n" + + " (1, 'Magic', '1990-10-01', '1');"; + List list = hiveDatasourceProcessor.splitAndRemoveComment(sql); + Assertions.assertEquals(list.size(), 2); + + } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-mysql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/mysql/param/MySQLDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-mysql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/mysql/param/MySQLDataSourceProcessor.java index b954defdd1c9..0c93b9821211 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-mysql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/mysql/param/MySQLDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-mysql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/mysql/param/MySQLDataSourceProcessor.java @@ -177,7 +177,8 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.mysql); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.mysql); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.mysql); } private static boolean checkKeyIsLegitimate(String key) { diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oceanbase/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oceanbase/param/OceanBaseDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oceanbase/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oceanbase/param/OceanBaseDataSourceProcessor.java index b07b543c4291..4d89f28eab1c 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oceanbase/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oceanbase/param/OceanBaseDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oceanbase/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oceanbase/param/OceanBaseDataSourceProcessor.java @@ -192,6 +192,7 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.oceanbase); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.oceanbase); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.oceanbase); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/param/PostgreSQLDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/param/PostgreSQLDataSourceProcessor.java index 2835d357ab04..28a0aa294578 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/param/PostgreSQLDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/param/PostgreSQLDataSourceProcessor.java @@ -131,7 +131,8 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.postgresql); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.postgresql); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.postgresql); } private String transformOther(Map otherMap) { diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sqlserver/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sqlserver/param/SQLServerDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sqlserver/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sqlserver/param/SQLServerDataSourceProcessor.java index 264f92c2b964..c3b73580dd22 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sqlserver/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sqlserver/param/SQLServerDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sqlserver/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sqlserver/param/SQLServerDataSourceProcessor.java @@ -128,7 +128,8 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.sqlserver); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.sqlserver); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.sqlserver); } private String transformOther(Map otherMap) { diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-trino/src/main/java/org/apache/dolphinscheduler/plugin/datasource/trino/param/TrinoDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-trino/src/main/java/org/apache/dolphinscheduler/plugin/datasource/trino/param/TrinoDataSourceProcessor.java index 77b10b51621b..bd53acaaf8fd 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-trino/src/main/java/org/apache/dolphinscheduler/plugin/datasource/trino/param/TrinoDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-trino/src/main/java/org/apache/dolphinscheduler/plugin/datasource/trino/param/TrinoDataSourceProcessor.java @@ -131,7 +131,8 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.trino); + String cleanSQL = SQLParserUtils.removeComment(sql, com.alibaba.druid.DbType.trino); + return SQLParserUtils.split(cleanSQL, com.alibaba.druid.DbType.trino); } private String transformOther(Map otherMap) { From 98bc9ce4c9d4e161f48617ac0a1efeb10f9d6968 Mon Sep 17 00:00:00 2001 From: Evan Sun Date: Thu, 4 Apr 2024 20:29:18 +0800 Subject: [PATCH 011/165] feat: inc coverage of alert plugin instance svc (#15799) Co-authored-by: abzymeinsjtu --- .../ApiFuncIdentificationConstant.java | 3 +- .../impl/AlertPluginInstanceServiceImpl.java | 6 +- .../AlertPluginInstanceServiceTest.java | 107 +++++++++++++++++- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/constants/ApiFuncIdentificationConstant.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/constants/ApiFuncIdentificationConstant.java index b93792307f73..480272bdc67b 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/constants/ApiFuncIdentificationConstant.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/constants/ApiFuncIdentificationConstant.java @@ -37,8 +37,7 @@ public class ApiFuncIdentificationConstant { public static final String TENANT_CREATE = "security:tenant:create"; public static final String TENANT_UPDATE = "security:tenant:update"; public static final String TENANT_DELETE = "security:tenant:delete"; - public static final String ALART_LIST = "monitor:alert:view"; - public static final String ALART_INSTANCE_CREATE = "security:alert-plugin:create"; + public static final String ALERT_INSTANCE_CREATE = "security:alert-plugin:create"; public static final String ALERT_PLUGIN_UPDATE = "security:alert-plugin:update"; public static final String ALERT_PLUGIN_DELETE = "security:alert-plugin:delete"; public static final String WORKER_GROUP_CREATE = "security:worker-group:create"; diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AlertPluginInstanceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AlertPluginInstanceServiceImpl.java index 59eaca3c83f3..cf07c9fd7342 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AlertPluginInstanceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AlertPluginInstanceServiceImpl.java @@ -17,7 +17,7 @@ package org.apache.dolphinscheduler.api.service.impl; -import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALART_INSTANCE_CREATE; +import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_INSTANCE_CREATE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_PLUGIN_DELETE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_PLUGIN_UPDATE; @@ -108,7 +108,7 @@ public AlertPluginInstance create(User loginUser, WarningType warningType, String pluginInstanceParams) { - if (!canOperatorPermissions(loginUser, null, AuthorizationType.ALERT_PLUGIN_INSTANCE, ALART_INSTANCE_CREATE)) { + if (!canOperatorPermissions(loginUser, null, AuthorizationType.ALERT_PLUGIN_INSTANCE, ALERT_INSTANCE_CREATE)) { throw new ServiceException(Status.USER_NO_OPERATION_PERM); } @@ -359,7 +359,7 @@ public void testSend(int pluginDefineId, String pluginInstanceParams) { throw new ServiceException(Status.ALERT_TEST_SENDING_FAILED, e.getMessage()); } - if (alertSendResponse.isSuccess()) { + if (!alertSendResponse.isSuccess()) { throw new ServiceException(Status.ALERT_TEST_SENDING_FAILED, alertSendResponse.getResResults().get(0).getMessage()); } diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/AlertPluginInstanceServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/AlertPluginInstanceServiceTest.java index 52910858d584..bd8a03995119 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/AlertPluginInstanceServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/AlertPluginInstanceServiceTest.java @@ -18,9 +18,11 @@ package org.apache.dolphinscheduler.api.service; import static org.apache.dolphinscheduler.api.AssertionsHelper.assertThrowsServiceException; -import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALART_INSTANCE_CREATE; +import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALARM_INSTANCE_MANAGE; +import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_INSTANCE_CREATE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_PLUGIN_DELETE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ALERT_PLUGIN_UPDATE; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.when; @@ -40,7 +42,6 @@ import org.apache.dolphinscheduler.dao.mapper.AlertGroupMapper; import org.apache.dolphinscheduler.dao.mapper.AlertPluginInstanceMapper; import org.apache.dolphinscheduler.dao.mapper.PluginDefineMapper; -import org.apache.dolphinscheduler.extract.alert.request.AlertSendResponse; import org.apache.dolphinscheduler.registry.api.RegistryClient; import org.apache.dolphinscheduler.registry.api.enums.RegistryNodeType; @@ -60,6 +61,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + /** * alert plugin instance service test */ @@ -90,12 +94,16 @@ public class AlertPluginInstanceServiceTest { private User user; + private User noPermUser; + private final AlertPluginInstanceType normalInstanceType = AlertPluginInstanceType.NORMAL; private final AlertPluginInstanceType globalInstanceType = AlertPluginInstanceType.GLOBAL; private final WarningType warningType = WarningType.ALL; + private final Integer GLOBAL_ALERT_GROUP_ID = 2; + private String uiParams = "[\n" + " {\n" + " \"field\":\"userParams\",\n" @@ -172,21 +180,33 @@ public class AlertPluginInstanceServiceTest { private String paramsMap = "{\"path\":\"/kris/script/path\",\"userParams\":\"userParams\",\"type\":\"0\"}"; + private AlertPluginInstance alertPluginInstance; + @BeforeEach public void before() { user = new User(); user.setUserType(UserType.ADMIN_USER); user.setId(1); - AlertPluginInstance alertPluginInstance = getAlertPluginInstance(1, normalInstanceType, "test"); + + noPermUser = new User(); + noPermUser.setUserType(UserType.GENERAL_USER); + noPermUser.setId(2); + + alertPluginInstance = getAlertPluginInstance(1, normalInstanceType, "test"); alertPluginInstances = new ArrayList<>(); alertPluginInstances.add(alertPluginInstance); } @Test public void testCreate() { + when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, + noPermUser.getId(), ALERT_INSTANCE_CREATE, baseServiceLogger)).thenReturn(false); + assertThrowsServiceException(Status.USER_NO_OPERATION_PERM, () -> alertPluginInstanceService.create(noPermUser, + 1, "test", normalInstanceType, warningType, uiParams)); + when(alertPluginInstanceMapper.existInstanceName("test")).thenReturn(true); when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, - 1, ALART_INSTANCE_CREATE, baseServiceLogger)).thenReturn(true); + 1, ALERT_INSTANCE_CREATE, baseServiceLogger)).thenReturn(true); when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, null, 0, baseServiceLogger)).thenReturn(true); assertThrowsServiceException(Status.PLUGIN_INSTANCE_ALREADY_EXISTS, @@ -195,6 +215,19 @@ public void testCreate() { AlertPluginInstance alertPluginInstance = alertPluginInstanceService.create(user, 1, "test1", normalInstanceType, warningType, uiParams); assertNotNull(alertPluginInstance); + + when(alertGroupMapper.selectById(GLOBAL_ALERT_GROUP_ID)).thenReturn(getGlobalAlertGroup()); + assertDoesNotThrow(() -> alertPluginInstanceService.create(user, 1, "global_plugin_instance", + AlertPluginInstanceType.GLOBAL, warningType, uiParams)); + + when(alertGroupMapper.selectById(GLOBAL_ALERT_GROUP_ID)).thenReturn(getGlobalAlertGroup("1")); + assertDoesNotThrow(() -> alertPluginInstanceService.create(user, 1, "global_plugin_instance", + AlertPluginInstanceType.GLOBAL, warningType, uiParams)); + + when(alertPluginInstanceMapper.insert(Mockito.any())).thenReturn(-1); + assertThrowsServiceException(Status.SAVE_ERROR, + () -> alertPluginInstanceService.create(user, 1, "test_insert_error", normalInstanceType, warningType, + uiParams)); } @Test @@ -202,11 +235,10 @@ public void testSendAlert() { Mockito.when(registryClient.getServerList(RegistryNodeType.ALERT_SERVER)).thenReturn(new ArrayList<>()); assertThrowsServiceException(Status.ALERT_SERVER_NOT_EXIST, () -> alertPluginInstanceService.testSend(1, uiParams)); - AlertSendResponse.AlertSendResponseResult alertResult = new AlertSendResponse.AlertSendResponseResult(); - alertResult.setSuccess(true); Server server = new Server(); server.setPort(50052); server.setHost("127.0.0.1"); + Mockito.when(registryClient.getServerList(RegistryNodeType.ALERT_SERVER)) .thenReturn(Collections.singletonList(server)); assertThrowsServiceException(Status.ALERT_TEST_SENDING_FAILED, @@ -215,6 +247,11 @@ public void testSendAlert() { @Test public void testDelete() { + when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, + noPermUser.getId(), ALERT_PLUGIN_DELETE, baseServiceLogger)).thenReturn(false); + assertThrowsServiceException(Status.USER_NO_OPERATION_PERM, + () -> alertPluginInstanceService.deleteById(noPermUser, 1)); + List ids = Arrays.asList("11,2,3", "5,96", null, "98,1"); when(alertGroupMapper.queryInstanceIdsList()).thenReturn(ids); when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, @@ -241,10 +278,18 @@ public void testDelete() { when(alertPluginInstanceMapper.deleteById(5)).thenReturn(1); Assertions.assertDoesNotThrow(() -> alertPluginInstanceService.deleteById(user, 5)); + + when(alertGroupMapper.queryInstanceIdsList()).thenReturn(Collections.emptyList()); + Assertions.assertDoesNotThrow(() -> alertPluginInstanceService.deleteById(user, 9)); } @Test public void testUpdate() { + when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, + noPermUser.getId(), ALERT_PLUGIN_UPDATE, baseServiceLogger)).thenReturn(false); + assertThrowsServiceException(Status.USER_NO_OPERATION_PERM, + () -> alertPluginInstanceService.updateById(noPermUser, 1, "test", warningType, uiParams)); + when(alertPluginInstanceMapper.updateById(Mockito.any())).thenReturn(0); when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, 1, ALERT_PLUGIN_UPDATE, baseServiceLogger)).thenReturn(true); @@ -259,8 +304,51 @@ public void testUpdate() { Assertions.assertNotNull(alertPluginInstance); } + @Test + public void testGetById() { + when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, + noPermUser.getId(), ALARM_INSTANCE_MANAGE, baseServiceLogger)).thenReturn(false); + assertThrowsServiceException(Status.USER_NO_OPERATION_PERM, + () -> alertPluginInstanceService.getById(noPermUser, 1)); + + when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, + user.getId(), ALARM_INSTANCE_MANAGE, baseServiceLogger)).thenReturn(true); + when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.ALERT_PLUGIN_INSTANCE, null, 0, + baseServiceLogger)).thenReturn(true); + when(alertPluginInstanceMapper.selectById(1)) + .thenReturn(getAlertPluginInstance(1, AlertPluginInstanceType.NORMAL, "test_get_instance")); + + Assertions.assertEquals(alertPluginInstanceService.getById(user, 1).getId(), 1); + } + + @Test + public void testCheckExistPluginInstanceName() { + when(alertPluginInstanceMapper.existInstanceName(Mockito.any(String.class))).thenReturn(false); + Assertions.assertEquals(false, alertPluginInstanceService.checkExistPluginInstanceName("test")); + } + + @Test + public void testListPaging() { + IPage page = new Page<>(); + page.setRecords(Collections.singletonList(alertPluginInstance)); + page.setTotal(1); + page.setPages(1); + + when(alertPluginInstanceMapper.queryByInstanceNamePage(Mockito.any(Page.class), Mockito.any(String.class))) + .thenReturn(page); + assertDoesNotThrow(() -> alertPluginInstanceService.listPaging(user, "test", 1, 1)); + } + @Test public void testQueryAll() { + when(alertPluginInstanceMapper.queryAllAlertPluginInstanceList()).thenReturn(Collections.emptyList()); + Assertions.assertEquals(0, alertPluginInstanceService.queryAll().size()); + + when(alertPluginInstanceMapper.queryAllAlertPluginInstanceList()) + .thenReturn(Collections.singletonList(alertPluginInstance)); + when(pluginDefineMapper.queryAllPluginDefineList()).thenReturn(Collections.emptyList()); + Assertions.assertEquals(0, alertPluginInstanceService.queryAll().size()); + AlertPluginInstance alertPluginInstance = getAlertPluginInstance(1, normalInstanceType, "test"); PluginDefine pluginDefine = new PluginDefine("script", "script", uiParams); pluginDefine.setId(1); @@ -283,4 +371,11 @@ private AlertPluginInstance getAlertPluginInstance(int id, AlertPluginInstanceTy return alertPluginInstance; } + private AlertGroup getGlobalAlertGroup(String... alertPluginInstanceIds) { + AlertGroup globalAlertGroup = new AlertGroup(); + globalAlertGroup.setId(2); + globalAlertGroup.setAlertInstanceIds(String.join(",", alertPluginInstanceIds)); + + return globalAlertGroup; + } } From 3b1de41acbee827b320370b14c6c73c7158f1aec Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 4 Apr 2024 23:44:12 +0800 Subject: [PATCH 012/165] Remove dolphinscheduler-data-quality from dolphinscheduler-task-dataquality (#15791) --- dolphinscheduler-api/pom.xml | 5 +++ .../quality/flow/batch/reader/JdbcReader.java | 8 +++- .../quality/flow/batch/writer/JdbcWriter.java | 8 +++- .../data/quality/utils/ParserUtilsTest.java | 39 ------------------- .../dolphinscheduler-task-dataquality/pom.xml | 5 --- .../plugin/task/dq/utils/RuleParserUtils.java | 23 +++++++---- 6 files changed, 32 insertions(+), 56 deletions(-) delete mode 100644 dolphinscheduler-data-quality/src/test/java/org/apache/dolphinscheduler/data/quality/utils/ParserUtilsTest.java diff --git a/dolphinscheduler-api/pom.xml b/dolphinscheduler-api/pom.xml index 681a85318abd..40d556a17ed3 100644 --- a/dolphinscheduler-api/pom.xml +++ b/dolphinscheduler-api/pom.xml @@ -61,6 +61,11 @@ dolphinscheduler-meter + + org.apache.dolphinscheduler + dolphinscheduler-data-quality + + org.apache.dolphinscheduler dolphinscheduler-datasource-all diff --git a/dolphinscheduler-data-quality/src/main/java/org/apache/dolphinscheduler/data/quality/flow/batch/reader/JdbcReader.java b/dolphinscheduler-data-quality/src/main/java/org/apache/dolphinscheduler/data/quality/flow/batch/reader/JdbcReader.java index 274d4f793a14..97ae41405146 100644 --- a/dolphinscheduler-data-quality/src/main/java/org/apache/dolphinscheduler/data/quality/flow/batch/reader/JdbcReader.java +++ b/dolphinscheduler-data-quality/src/main/java/org/apache/dolphinscheduler/data/quality/flow/batch/reader/JdbcReader.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.data.quality.flow.batch.reader; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.dolphinscheduler.data.quality.Constants.DATABASE; import static org.apache.dolphinscheduler.data.quality.Constants.DB_TABLE; import static org.apache.dolphinscheduler.data.quality.Constants.DOTS; @@ -32,17 +33,19 @@ import org.apache.dolphinscheduler.data.quality.execution.SparkRuntimeEnvironment; import org.apache.dolphinscheduler.data.quality.flow.batch.BatchReader; import org.apache.dolphinscheduler.data.quality.utils.ConfigUtils; -import org.apache.dolphinscheduler.data.quality.utils.ParserUtils; import org.apache.spark.sql.DataFrameReader; import org.apache.spark.sql.Dataset; import org.apache.spark.sql.Row; import org.apache.spark.sql.SparkSession; +import java.net.URLDecoder; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import lombok.SneakyThrows; + /** * AbstractJdbcSource */ @@ -74,6 +77,7 @@ public Dataset read(SparkRuntimeEnvironment env) { return jdbcReader(env.sparkSession()).load(); } + @SneakyThrows private DataFrameReader jdbcReader(SparkSession sparkSession) { DataFrameReader reader = sparkSession.read() @@ -81,7 +85,7 @@ private DataFrameReader jdbcReader(SparkSession sparkSession) { .option(URL, config.getString(URL)) .option(DB_TABLE, config.getString(DATABASE) + "." + config.getString(TABLE)) .option(USER, config.getString(USER)) - .option(PASSWORD, ParserUtils.decode(config.getString(PASSWORD))) + .option(PASSWORD, URLDecoder.decode(config.getString(PASSWORD), UTF_8.name())) .option(DRIVER, config.getString(DRIVER)); Config jdbcConfig = ConfigUtils.extractSubConfig(config, JDBC + DOTS, false); diff --git a/dolphinscheduler-data-quality/src/main/java/org/apache/dolphinscheduler/data/quality/flow/batch/writer/JdbcWriter.java b/dolphinscheduler-data-quality/src/main/java/org/apache/dolphinscheduler/data/quality/flow/batch/writer/JdbcWriter.java index 07b2bd60d562..b737567f2147 100644 --- a/dolphinscheduler-data-quality/src/main/java/org/apache/dolphinscheduler/data/quality/flow/batch/writer/JdbcWriter.java +++ b/dolphinscheduler-data-quality/src/main/java/org/apache/dolphinscheduler/data/quality/flow/batch/writer/JdbcWriter.java @@ -33,13 +33,16 @@ import org.apache.dolphinscheduler.data.quality.config.ValidateResult; import org.apache.dolphinscheduler.data.quality.execution.SparkRuntimeEnvironment; import org.apache.dolphinscheduler.data.quality.flow.batch.BatchWriter; -import org.apache.dolphinscheduler.data.quality.utils.ParserUtils; import org.apache.spark.sql.Dataset; import org.apache.spark.sql.Row; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import lombok.SneakyThrows; + import com.google.common.base.Strings; /** @@ -70,6 +73,7 @@ public void prepare(SparkRuntimeEnvironment prepareEnv) { } } + @SneakyThrows @Override public void write(Dataset data, SparkRuntimeEnvironment env) { if (!Strings.isNullOrEmpty(config.getString(SQL))) { @@ -82,7 +86,7 @@ public void write(Dataset data, SparkRuntimeEnvironment env) { .option(URL, config.getString(URL)) .option(DB_TABLE, config.getString(DATABASE) + "." + config.getString(TABLE)) .option(USER, config.getString(USER)) - .option(PASSWORD, ParserUtils.decode(config.getString(PASSWORD))) + .option(PASSWORD, URLDecoder.decode(config.getString(PASSWORD), StandardCharsets.UTF_8.name())) .mode(config.getString(SAVE_MODE)) .save(); } diff --git a/dolphinscheduler-data-quality/src/test/java/org/apache/dolphinscheduler/data/quality/utils/ParserUtilsTest.java b/dolphinscheduler-data-quality/src/test/java/org/apache/dolphinscheduler/data/quality/utils/ParserUtilsTest.java deleted file mode 100644 index 328316cc391f..000000000000 --- a/dolphinscheduler-data-quality/src/test/java/org/apache/dolphinscheduler/data/quality/utils/ParserUtilsTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.data.quality.utils; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ParserUtilsTest { - - @Test - public void testParserUtils() { - String testStr = "aaa$bbb$ccc%ddd^eee#fff"; - String encode = ParserUtils.encode(testStr); - String decode = ParserUtils.decode(encode); - Assertions.assertEquals(testStr, decode); - - String blank = ""; - Assertions.assertEquals(ParserUtils.encode(blank), blank); - Assertions.assertEquals(ParserUtils.decode(blank), blank); - - Assertions.assertNull(ParserUtils.encode(null)); - Assertions.assertNull(ParserUtils.decode(null)); - } -} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-dataquality/pom.xml b/dolphinscheduler-task-plugin/dolphinscheduler-task-dataquality/pom.xml index cc64b6cdcb49..6626234a19ac 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-dataquality/pom.xml +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-dataquality/pom.xml @@ -43,11 +43,6 @@ dolphinscheduler-datasource-all ${project.version} - - org.apache.dolphinscheduler - dolphinscheduler-data-quality - ${project.version} - org.apache.commons commons-collections4 diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-dataquality/src/main/java/org/apache/dolphinscheduler/plugin/task/dq/utils/RuleParserUtils.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-dataquality/src/main/java/org/apache/dolphinscheduler/plugin/task/dq/utils/RuleParserUtils.java index e99bb3dfafd9..185573f66e3d 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-dataquality/src/main/java/org/apache/dolphinscheduler/plugin/task/dq/utils/RuleParserUtils.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-dataquality/src/main/java/org/apache/dolphinscheduler/plugin/task/dq/utils/RuleParserUtils.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.plugin.task.dq.utils; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.dolphinscheduler.plugin.task.api.TaskConstants.PARAMETER_BUSINESS_DATE; import static org.apache.dolphinscheduler.plugin.task.api.TaskConstants.PARAMETER_CURRENT_DATE; import static org.apache.dolphinscheduler.plugin.task.api.TaskConstants.PARAMETER_DATETIME; @@ -62,7 +63,6 @@ import static org.apache.dolphinscheduler.plugin.task.api.utils.DataQualityConstants.USER; import org.apache.dolphinscheduler.common.utils.JSONUtils; -import org.apache.dolphinscheduler.data.quality.utils.ParserUtils; import org.apache.dolphinscheduler.plugin.datasource.api.utils.DataSourceUtils; import org.apache.dolphinscheduler.plugin.task.api.DataQualityTaskExecutionContext; import org.apache.dolphinscheduler.plugin.task.api.enums.dp.ExecuteSqlType; @@ -80,12 +80,15 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import lombok.SneakyThrows; + import com.fasterxml.jackson.databind.node.ArrayNode; /** @@ -102,9 +105,10 @@ private RuleParserUtils() { private static final String AND_TARGET_FILTER = "AND (${target_filter})"; private static final String WHERE_TARGET_FILTER = "WHERE (${target_filter})"; + @SneakyThrows public static List getReaderConfigList( Map inputParameterValue, - DataQualityTaskExecutionContext dataQualityTaskExecutionContext) throws DataQualityException { + DataQualityTaskExecutionContext dataQualityTaskExecutionContext) { List readerConfigList = new ArrayList<>(); @@ -123,7 +127,7 @@ public static List getReaderConfigList( config.put(URL, DataSourceUtils.getJdbcUrl(DbType.of(dataQualityTaskExecutionContext.getSourceType()), sourceDataSource)); config.put(USER, sourceDataSource.getUser()); - config.put(PASSWORD, ParserUtils.encode(sourceDataSource.getPassword())); + config.put(PASSWORD, URLEncoder.encode(sourceDataSource.getPassword(), UTF_8.name())); config.put(DRIVER, DataSourceUtils .getDatasourceDriver(DbType.of(dataQualityTaskExecutionContext.getSourceType()))); String outputTable = inputParameterValue.get(SRC_DATABASE) + "_" + inputParameterValue.get(SRC_TABLE); @@ -150,7 +154,7 @@ public static List getReaderConfigList( config.put(URL, DataSourceUtils.getJdbcUrl(DbType.of(dataQualityTaskExecutionContext.getTargetType()), targetDataSource)); config.put(USER, targetDataSource.getUser()); - config.put(PASSWORD, ParserUtils.encode(targetDataSource.getPassword())); + config.put(PASSWORD, URLEncoder.encode(targetDataSource.getPassword(), UTF_8.name())); config.put(DRIVER, DataSourceUtils .getDatasourceDriver(DbType.of(dataQualityTaskExecutionContext.getTargetType()))); String outputTable = @@ -264,9 +268,10 @@ public static Map getInputParameterMapFromEntryList(List getWriterConfigList( String sql, - DataQualityTaskExecutionContext dataQualityTaskExecutionContext) throws DataQualityException { + DataQualityTaskExecutionContext dataQualityTaskExecutionContext) { List writerConfigList = new ArrayList<>(); if (StringUtils.isNotEmpty(dataQualityTaskExecutionContext.getWriterConnectorType())) { @@ -284,7 +289,7 @@ public static List getWriterConfigList( config.put(URL, DataSourceUtils.getJdbcUrl(DbType.of(dataQualityTaskExecutionContext.getWriterType()), writerDataSource)); config.put(USER, writerDataSource.getUser()); - config.put(PASSWORD, ParserUtils.encode(writerDataSource.getPassword())); + config.put(PASSWORD, URLEncoder.encode(writerDataSource.getPassword(), UTF_8.name())); config.put(DRIVER, DataSourceUtils .getDatasourceDriver(DbType.of(dataQualityTaskExecutionContext.getWriterType()))); config.put(SQL, sql); @@ -336,8 +341,9 @@ public static List getStatisticsValueConfigReaderList( return readerConfigList; } + @SneakyThrows public static BaseConfig getStatisticsValueConfig( - DataQualityTaskExecutionContext dataQualityTaskExecutionContext) throws DataQualityException { + DataQualityTaskExecutionContext dataQualityTaskExecutionContext) { BaseConfig baseConfig = null; if (StringUtils.isNotEmpty(dataQualityTaskExecutionContext.getStatisticsValueConnectorType())) { BaseConnectionParam writerDataSource = @@ -354,7 +360,7 @@ public static BaseConfig getStatisticsValueConfig( config.put(URL, DataSourceUtils.getJdbcUrl( DbType.of(dataQualityTaskExecutionContext.getStatisticsValueType()), writerDataSource)); config.put(USER, writerDataSource.getUser()); - config.put(PASSWORD, ParserUtils.encode(writerDataSource.getPassword())); + config.put(PASSWORD, URLEncoder.encode(writerDataSource.getPassword(), UTF_8.name())); config.put(DRIVER, DataSourceUtils .getDatasourceDriver(DbType.of(dataQualityTaskExecutionContext.getWriterType()))); } @@ -544,6 +550,7 @@ public static BaseConfig getErrorOutputWriter(Map inputParameter /** * the unique code use to get the same type and condition task statistics value + * * @param inputParameterValue * @return */ From 200d23fc3ebc67058fb49167b41b95c1eb62be8d Mon Sep 17 00:00:00 2001 From: Evan Sun Date: Sun, 7 Apr 2024 09:58:28 +0800 Subject: [PATCH 013/165] [TEST] increase coverage of datasource service (#15801) Co-authored-by: abzymeinsjtu --- .../service/impl/DataSourceServiceImpl.java | 6 +- .../api/service/DataSourceServiceTest.java | 144 ++++++++++++++---- .../dao/mapper/DataSourceMapper.java | 3 +- 3 files changed, 121 insertions(+), 32 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/DataSourceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/DataSourceServiceImpl.java index a0307624578c..210900e54c5a 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/DataSourceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/DataSourceServiceImpl.java @@ -230,7 +230,7 @@ public BaseDataSourceParamDTO queryDataSource(int id, User loginUser) { @Override public PageInfo queryDataSourceListPaging(User loginUser, String searchVal, Integer pageNo, Integer pageSize) { - IPage dataSourceList = null; + IPage dataSourceList; Page dataSourcePage = new Page<>(pageNo, pageSize); PageInfo pageInfo = new PageInfo<>(pageNo, pageSize); if (loginUser.getUserType().equals(UserType.ADMIN_USER)) { @@ -282,7 +282,7 @@ private String getHiddenPassword() { @Override public List queryDataSourceList(User loginUser, Integer type) { - List datasourceList = null; + List datasourceList; if (loginUser.getUserType().equals(UserType.ADMIN_USER)) { datasourceList = dataSourceMapper.queryDataSourceByType(0, type); } else { @@ -420,7 +420,7 @@ public List authedDatasource(User loginUser, Integer userId) { public List getTables(Integer datasourceId, String database) { DataSource dataSource = dataSourceMapper.selectById(datasourceId); - List tableList = null; + List tableList; BaseConnectionParam connectionParam = (BaseConnectionParam) DataSourceUtils.buildConnectionParams( dataSource.getType(), diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/DataSourceServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/DataSourceServiceTest.java index 68d4db0ff2ad..b4e7aae4b8d3 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/DataSourceServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/DataSourceServiceTest.java @@ -52,15 +52,15 @@ import org.apache.commons.collections4.CollectionUtils; +import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.Random; import java.util.concurrent.ExecutionException; import org.junit.jupiter.api.Assertions; @@ -73,6 +73,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.DuplicateKeyException; + +import com.baomidou.mybatisplus.core.metadata.IPage; /** * data source service test @@ -96,6 +99,15 @@ public class DataSourceServiceTest { @Mock private ResourcePermissionCheckService resourcePermissionCheckService; + @Mock + private IPage dataSourceList; + + private String randomStringWithLengthN(int n) { + byte[] bitArray = new byte[n]; + new Random().nextBytes(bitArray); + return new String(bitArray, StandardCharsets.UTF_8); + } + private void passResourcePermissionCheckService() { when(resourcePermissionCheckService.operationPermissionCheck(Mockito.any(), Mockito.anyInt(), Mockito.anyString(), Mockito.any())).thenReturn(true); @@ -131,6 +143,7 @@ public void createDataSourceTest() throws ExecutionException { when(dataSourceMapper.queryDataSourceByName(dataSourceName.trim())).thenReturn(dataSourceList); passResourcePermissionCheckService(); + // DATASOURCE_EXIST assertThrowsServiceException(Status.DATASOURCE_EXIST, () -> dataSourceService.createDataSource(loginUser, postgreSqlDatasourceParam)); @@ -140,13 +153,24 @@ public void createDataSourceTest() throws ExecutionException { when(dataSourceMapper.queryDataSourceByName(dataSourceName.trim())).thenReturn(null); + // DESCRIPTION TOO LONG + postgreSqlDatasourceParam.setNote(randomStringWithLengthN(512)); + assertThrowsServiceException(Status.DESCRIPTION_TOO_LONG_ERROR, + () -> dataSourceService.createDataSource(loginUser, postgreSqlDatasourceParam)); + postgreSqlDatasourceParam.setNote(dataSourceDesc); + // SUCCESS assertDoesNotThrow(() -> dataSourceService.createDataSource(loginUser, postgreSqlDatasourceParam)); + + // Duplicated Key Exception + when(dataSourceMapper.insert(Mockito.any(DataSource.class))).thenThrow(DuplicateKeyException.class); + assertThrowsServiceException(Status.DATASOURCE_EXIST, + () -> dataSourceService.createDataSource(loginUser, postgreSqlDatasourceParam)); } } @Test - public void updateDataSourceTest() throws ExecutionException { + public void updateDataSourceTest() { User loginUser = getAdminUser(); int dataSourceId = 12; @@ -200,32 +224,74 @@ public void updateDataSourceTest() throws ExecutionException { // DATASOURCE_CONNECT_FAILED when(dataSourceMapper.queryDataSourceByName(postgreSqlDatasourceParam.getName())).thenReturn(null); + // DESCRIPTION TOO LONG + postgreSqlDatasourceParam.setNote(randomStringWithLengthN(512)); + assertThrowsServiceException(Status.DESCRIPTION_TOO_LONG_ERROR, + () -> dataSourceService.updateDataSource(loginUser, postgreSqlDatasourceParam)); + postgreSqlDatasourceParam.setNote(dataSourceDesc); + // SUCCESS assertDoesNotThrow(() -> dataSourceService.updateDataSource(loginUser, postgreSqlDatasourceParam)); + + // Duplicated Key Exception + when(dataSourceMapper.updateById(Mockito.any(DataSource.class))).thenThrow(DuplicateKeyException.class); + assertThrowsServiceException(Status.DATASOURCE_EXIST, + () -> dataSourceService.updateDataSource(loginUser, postgreSqlDatasourceParam)); } } @Test - public void queryDataSourceListPagingTest() { - User loginUser = getAdminUser(); + public void testQueryDataSourceListPaging() { + + User adminUser = getAdminUser(); + User generalUser = getGeneralUser(); String searchVal = ""; int pageNo = 1; int pageSize = 10; PageInfo pageInfo = - dataSourceService.queryDataSourceListPaging(loginUser, searchVal, pageNo, pageSize); + dataSourceService.queryDataSourceListPaging(adminUser, searchVal, pageNo, pageSize); Assertions.assertNotNull(pageInfo); + + // test query datasource as general user with no datasource authed + when(dataSourceList.getRecords()).thenReturn(getSingleDataSourceList()); + when(dataSourceMapper.selectPagingByIds(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(dataSourceList); + assertDoesNotThrow(() -> dataSourceService.queryDataSourceListPaging(generalUser, searchVal, pageNo, pageSize)); + + // test query datasource as general user with datasource authed + when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.DATASOURCE, + generalUser.getId(), dataSourceServiceLogger)).thenReturn(Collections.singleton(1)); + + assertDoesNotThrow(() -> dataSourceService.queryDataSourceListPaging(generalUser, searchVal, pageNo, pageSize)); } @Test - public void connectionTest() { + public void testConnectionTest() { int dataSourceId = -1; when(dataSourceMapper.selectById(dataSourceId)).thenReturn(null); assertThrowsServiceException(Status.RESOURCE_NOT_EXIST, () -> dataSourceService.connectionTest(dataSourceId)); + + try ( + MockedStatic ignored = + Mockito.mockStatic(DataSourceUtils.class)) { + DataSource dataSource = getOracleDataSource(999); + when(dataSourceMapper.selectById(dataSource.getId())).thenReturn(dataSource); + DataSourceProcessor dataSourceProcessor = Mockito.mock(DataSourceProcessor.class); + + when(DataSourceUtils.getDatasourceProcessor(Mockito.any())).thenReturn(dataSourceProcessor); + when(dataSourceProcessor.checkDataSourceConnectivity(Mockito.any())).thenReturn(true); + assertDoesNotThrow(() -> dataSourceService.connectionTest(dataSource.getId())); + + when(dataSourceProcessor.checkDataSourceConnectivity(Mockito.any())).thenReturn(false); + assertThrowsServiceException(Status.CONNECTION_TEST_FAILURE, + () -> dataSourceService.connectionTest(dataSource.getId())); + } + } @Test - public void deleteTest() { + public void testDelete() { User loginUser = getAdminUser(); int dataSourceId = 1; // resource not exist @@ -252,7 +318,7 @@ public void deleteTest() { } @Test - public void unauthDatasourceTest() { + public void testUnAuthDatasource() { User loginUser = getAdminUser(); loginUser.setId(1); loginUser.setUserType(UserType.ADMIN_USER); @@ -279,7 +345,7 @@ public void unauthDatasourceTest() { } @Test - public void authedDatasourceTest() { + public void testAuthedDatasource() { User loginUser = getAdminUser(); loginUser.setId(1); loginUser.setUserType(UserType.ADMIN_USER); @@ -300,19 +366,28 @@ public void authedDatasourceTest() { } @Test - public void queryDataSourceListTest() { - User loginUser = new User(); - loginUser.setUserType(UserType.GENERAL_USER); - Set dataSourceIds = new HashSet<>(); - dataSourceIds.add(1); + public void testQueryDataSourceList() { + User adminUser = getAdminUser(); + assertDoesNotThrow(() -> dataSourceService.queryDataSourceList(adminUser, DbType.MYSQL.ordinal())); + + User generalUser = getGeneralUser(); + when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.DATASOURCE, - loginUser.getId(), dataSourceServiceLogger)).thenReturn(dataSourceIds); + generalUser.getId(), dataSourceServiceLogger)).thenReturn(Collections.emptySet()); + List emptyList = dataSourceService.queryDataSourceList(generalUser, DbType.MYSQL.ordinal()); + Assertions.assertEquals(emptyList.size(), 0); + + when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.DATASOURCE, + generalUser.getId(), dataSourceServiceLogger)).thenReturn(Collections.singleton(1)); DataSource dataSource = new DataSource(); + dataSource.setId(1); dataSource.setType(DbType.MYSQL); - when(dataSourceMapper.selectBatchIds(dataSourceIds)).thenReturn(Collections.singletonList(dataSource)); + when(dataSourceMapper.selectBatchIds(Collections.singleton(1))) + .thenReturn(Collections.singletonList(dataSource)); + List list = - dataSourceService.queryDataSourceList(loginUser, DbType.MYSQL.ordinal()); + dataSourceService.queryDataSourceList(generalUser, DbType.MYSQL.ordinal()); Assertions.assertNotNull(list); } @@ -327,21 +402,28 @@ public void verifyDataSourceNameTest() { } @Test - public void queryDataSourceTest() { - when(dataSourceMapper.selectById(Mockito.anyInt())).thenReturn(null); + public void testQueryDataSource() { + // datasource not exists + when(dataSourceMapper.selectById(999)).thenReturn(null); User loginUser = new User(); loginUser.setUserType(UserType.GENERAL_USER); loginUser.setId(2); - try { - dataSourceService.queryDataSource(Mockito.anyInt(), loginUser); - } catch (Exception e) { - Assertions.assertTrue(e.getMessage().contains(Status.RESOURCE_NOT_EXIST.getMsg())); - } + + assertThrowsServiceException(Status.RESOURCE_NOT_EXIST, + () -> dataSourceService.queryDataSource(999, loginUser)); DataSource dataSource = getOracleDataSource(1); - when(dataSourceMapper.selectById(Mockito.anyInt())).thenReturn(dataSource); + when(dataSourceMapper.selectById(dataSource.getId())).thenReturn(dataSource); when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.DATASOURCE, loginUser.getId(), DATASOURCE, baseServiceLogger)).thenReturn(true); + + // no perm + when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.DATASOURCE, + new Object[]{dataSource.getId()}, loginUser.getId(), baseServiceLogger)).thenReturn(false); + assertThrowsServiceException(Status.USER_NO_OPERATION_PERM, + () -> dataSourceService.queryDataSource(dataSource.getId(), loginUser)); + + // success when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.DATASOURCE, new Object[]{dataSource.getId()}, loginUser.getId(), baseServiceLogger)).thenReturn(true); BaseDataSourceParamDTO paramDTO = dataSourceService.queryDataSource(dataSource.getId(), loginUser); @@ -472,8 +554,16 @@ public void buildParameterWithDecodePassword() { */ private User getAdminUser() { User loginUser = new User(); - loginUser.setId(-1); + loginUser.setId(1); loginUser.setUserName("admin"); + loginUser.setUserType(UserType.ADMIN_USER); + return loginUser; + } + + private User getGeneralUser() { + User loginUser = new User(); + loginUser.setId(2); + loginUser.setUserName("user"); loginUser.setUserType(UserType.GENERAL_USER); return loginUser; } diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/DataSourceMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/DataSourceMapper.java index b5dcc316274b..e26fe77ae49c 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/DataSourceMapper.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/DataSourceMapper.java @@ -102,8 +102,7 @@ List listAuthorizedDataSource(@Param("userId") int userId, /** * selectPagingByIds * @param dataSourcePage - * @param ids - * @param searchVal + * @param dataSourceIds * @return */ IPage selectPagingByIds(Page dataSourcePage, From 39274094f8889ea0c21bc5b273052da8cd58a616 Mon Sep 17 00:00:00 2001 From: John Huang Date: Sun, 7 Apr 2024 15:28:21 +0800 Subject: [PATCH 014/165] Fix typo on common.properties (#15806) --- deploy/kubernetes/dolphinscheduler/templates/configmap.yaml | 2 +- .../templates/deployment-dolphinscheduler-alert.yaml | 2 +- .../templates/deployment-dolphinscheduler-api.yaml | 2 +- .../templates/statefulset-dolphinscheduler-master.yaml | 2 +- .../templates/statefulset-dolphinscheduler-worker.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deploy/kubernetes/dolphinscheduler/templates/configmap.yaml b/deploy/kubernetes/dolphinscheduler/templates/configmap.yaml index 66ea53854bfa..c9af1c00a59d 100644 --- a/deploy/kubernetes/dolphinscheduler/templates/configmap.yaml +++ b/deploy/kubernetes/dolphinscheduler/templates/configmap.yaml @@ -32,7 +32,7 @@ data: {{- end }} {{- end }} {{- end }} - common_properties: |- + common.properties: |- {{- if index .Values.conf "common" }} {{- range $key, $value := index .Values.conf "common" }} {{- if and $.Values.minio.enabled }} diff --git a/deploy/kubernetes/dolphinscheduler/templates/deployment-dolphinscheduler-alert.yaml b/deploy/kubernetes/dolphinscheduler/templates/deployment-dolphinscheduler-alert.yaml index 41e01ef386a9..54316faeed6a 100644 --- a/deploy/kubernetes/dolphinscheduler/templates/deployment-dolphinscheduler-alert.yaml +++ b/deploy/kubernetes/dolphinscheduler/templates/deployment-dolphinscheduler-alert.yaml @@ -114,7 +114,7 @@ spec: name: {{ include "dolphinscheduler.fullname" . }}-alert - name: config-volume mountPath: /opt/dolphinscheduler/conf/common.properties - subPath: common_properties + subPath: common.properties volumes: - name: {{ include "dolphinscheduler.fullname" . }}-alert {{- if .Values.alert.persistentVolumeClaim.enabled }} diff --git a/deploy/kubernetes/dolphinscheduler/templates/deployment-dolphinscheduler-api.yaml b/deploy/kubernetes/dolphinscheduler/templates/deployment-dolphinscheduler-api.yaml index c2770cfd98c5..47bd4c3ca922 100644 --- a/deploy/kubernetes/dolphinscheduler/templates/deployment-dolphinscheduler-api.yaml +++ b/deploy/kubernetes/dolphinscheduler/templates/deployment-dolphinscheduler-api.yaml @@ -115,7 +115,7 @@ spec: name: {{ include "dolphinscheduler.fullname" . }}-api - name: config-volume mountPath: /opt/dolphinscheduler/conf/common.properties - subPath: common_properties + subPath: common.properties {{- if .Values.api.taskTypeFilter.enabled }} - name: config-volume mountPath: /opt/dolphinscheduler/conf/task-type-config.yaml diff --git a/deploy/kubernetes/dolphinscheduler/templates/statefulset-dolphinscheduler-master.yaml b/deploy/kubernetes/dolphinscheduler/templates/statefulset-dolphinscheduler-master.yaml index 888c35607eb1..66b4f8f8ee94 100644 --- a/deploy/kubernetes/dolphinscheduler/templates/statefulset-dolphinscheduler-master.yaml +++ b/deploy/kubernetes/dolphinscheduler/templates/statefulset-dolphinscheduler-master.yaml @@ -112,7 +112,7 @@ spec: {{- include "dolphinscheduler.sharedStorage.volumeMount" . | nindent 12 }} - name: config-volume mountPath: /opt/dolphinscheduler/conf/common.properties - subPath: common_properties + subPath: common.properties {{- include "dolphinscheduler.etcd.ssl.volumeMount" . | nindent 12 }} volumes: - name: {{ include "dolphinscheduler.fullname" . }}-master diff --git a/deploy/kubernetes/dolphinscheduler/templates/statefulset-dolphinscheduler-worker.yaml b/deploy/kubernetes/dolphinscheduler/templates/statefulset-dolphinscheduler-worker.yaml index 7a75849faea5..231f943fa68f 100644 --- a/deploy/kubernetes/dolphinscheduler/templates/statefulset-dolphinscheduler-worker.yaml +++ b/deploy/kubernetes/dolphinscheduler/templates/statefulset-dolphinscheduler-worker.yaml @@ -113,7 +113,7 @@ spec: name: {{ include "dolphinscheduler.fullname" . }}-worker-logs - name: config-volume mountPath: /opt/dolphinscheduler/conf/common.properties - subPath: common_properties + subPath: common.properties {{- include "dolphinscheduler.sharedStorage.volumeMount" . | nindent 12 }} {{- include "dolphinscheduler.fsFileResource.volumeMount" . | nindent 12 }} {{- include "dolphinscheduler.etcd.ssl.volumeMount" . | nindent 12 }} From 9599b5aa815300a49d74a2513be3bfa11366742a Mon Sep 17 00:00:00 2001 From: songwenyong <119404633+songwenyong@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:57:52 +0800 Subject: [PATCH 015/165] =?UTF-8?q?fix=EF=BC=9ADataSource=20And=20UdfFunc?= =?UTF-8?q?=20list=20query=20use=20Enum=20code=20value=20not=20ordinal=20(?= =?UTF-8?q?#15714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rick Cheng --- .../dolphinscheduler/api/controller/DataSourceController.java | 2 +- .../dolphinscheduler/api/controller/ResourcesController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/DataSourceController.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/DataSourceController.java index 4d1f0f9f3d70..d4fa83dd3a4b 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/DataSourceController.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/DataSourceController.java @@ -162,7 +162,7 @@ public Result queryDataSource(@Parameter(hidden = true) @RequestAttribut @ApiException(QUERY_DATASOURCE_ERROR) public Result queryDataSourceList(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, @RequestParam("type") DbType type) { - List datasourceList = dataSourceService.queryDataSourceList(loginUser, type.ordinal()); + List datasourceList = dataSourceService.queryDataSourceList(loginUser, type.getCode()); return Result.success(datasourceList); } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ResourcesController.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ResourcesController.java index 773e734c22c2..e9c28a5f6e2a 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ResourcesController.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ResourcesController.java @@ -554,7 +554,7 @@ public Result queryUdfFuncListPaging(@Parameter(hidden = true) @RequestA @ApiException(QUERY_DATASOURCE_BY_TYPE_ERROR) public Result queryUdfFuncList(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, @RequestParam("type") UdfType type) { - return udfFuncService.queryUdfFuncList(loginUser, type.ordinal()); + return udfFuncService.queryUdfFuncList(loginUser, type.getCode()); } /** From 66df5d4b907d74a5ba0d663f12222cb973f6ff5e Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Tue, 9 Apr 2024 14:04:24 +0800 Subject: [PATCH 016/165] Split cpuUsage to systemCpuUsage and jvmCpuUsage (#15803) --- deploy/kubernetes/dolphinscheduler/README.md | 8 +- .../kubernetes/dolphinscheduler/values.yaml | 16 +- docs/docs/en/architecture/configuration.md | 234 +++++++++--------- docs/docs/zh/architecture/configuration.md | 176 +++++++------ .../alert/registry/AlertHeartbeatTask.java | 3 +- .../common/model/AlertServerHeartBeat.java | 28 +-- .../common/model/BaseHeartBeat.java | 46 ++++ .../common/model/HeartBeat.java | 4 - .../common/model/MasterHeartBeat.java | 28 +-- .../common/model/WorkerHeartBeat.java | 29 +-- .../server/master/MasterServer.java | 2 +- .../config/MasterServerLoadProtection.java | 50 +--- .../master/metrics/MasterServerMetrics.java | 2 +- .../master/registry/MasterSlotManager.java | 3 +- .../master/registry/ServerNodeManager.java | 4 +- .../master/task/MasterHeartBeatTask.java | 3 +- .../src/main/resources/application.yaml | 8 +- .../master/config/MasterConfigTest.java | 25 +- .../MasterServerLoadProtectionTest.java | 3 +- .../src/test/resources/application.yaml | 164 ++++++++++++ .../src/test/resources/logback.xml | 8 +- .../metrics/BaseServerLoadProtection.java | 67 +++++ .../meter/metrics/DefaultMetricsProvider.java | 11 +- .../meter/metrics/ServerLoadProtection.java | 24 ++ .../meter/metrics/SystemMetrics.java | 3 +- .../src/main/resources/application.yaml | 16 +- .../server/worker/WorkerServer.java | 2 +- .../config/WorkerServerLoadProtection.java | 50 +--- .../worker/task/WorkerHeartBeatTask.java | 3 +- .../src/main/resources/application.yaml | 8 +- .../WorkerServerLoadProtectionTest.java | 3 +- 31 files changed, 603 insertions(+), 428 deletions(-) create mode 100644 dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/BaseHeartBeat.java create mode 100644 dolphinscheduler-master/src/test/resources/application.yaml create mode 100644 dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/BaseServerLoadProtection.java create mode 100644 dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/ServerLoadProtection.java diff --git a/deploy/kubernetes/dolphinscheduler/README.md b/deploy/kubernetes/dolphinscheduler/README.md index 5659605b957a..33633f3b2e18 100644 --- a/deploy/kubernetes/dolphinscheduler/README.md +++ b/deploy/kubernetes/dolphinscheduler/README.md @@ -200,9 +200,9 @@ Please refer to the [Quick Start in Kubernetes](../../../docs/docs/en/guide/inst | master.env.MASTER_KILL_APPLICATION_WHEN_HANDLE_FAILOVER | string | `"true"` | Master kill application when handle failover | | master.env.MASTER_MAX_HEARTBEAT_INTERVAL | string | `"10s"` | Master max heartbeat interval | | master.env.MASTER_SERVER_LOAD_PROTECTION_ENABLED | bool | `false` | If set true, will open master overload protection | -| master.env.MASTER_SERVER_LOAD_PROTECTION_MAX_CPU_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Master max cpu usage, when the master's cpu usage is smaller then this value, master server can execute workflow. | | master.env.MASTER_SERVER_LOAD_PROTECTION_MAX_DISK_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Master max disk usage , when the master's disk usage is smaller then this value, master server can execute workflow. | -| master.env.MASTER_SERVER_LOAD_PROTECTION_MAX_JVM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Master max JVM memory usage , when the master's jvm memory usage is smaller then this value, master server can execute workflow. | +| master.env.MASTER_SERVER_LOAD_PROTECTION_MAX_JVM_CPU_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Master max jvm cpu usage, when the master's jvm cpu usage is smaller then this value, master server can execute workflow. | +| master.env.MASTER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_CPU_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Master max system cpu usage, when the master's system cpu usage is smaller then this value, master server can execute workflow. | | master.env.MASTER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Master max System memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. | | master.env.MASTER_STATE_WHEEL_INTERVAL | string | `"5s"` | master state wheel interval, the unit is second | | master.env.MASTER_TASK_COMMIT_INTERVAL | string | `"1s"` | master commit task interval, the unit is second | @@ -301,9 +301,9 @@ Please refer to the [Quick Start in Kubernetes](../../../docs/docs/en/guide/inst | worker.env.WORKER_HOST_WEIGHT | string | `"100"` | Worker host weight to dispatch tasks | | worker.env.WORKER_MAX_HEARTBEAT_INTERVAL | string | `"10s"` | Worker heartbeat interval | | worker.env.WORKER_SERVER_LOAD_PROTECTION_ENABLED | bool | `false` | If set true, will open worker overload protection | -| worker.env.WORKER_SERVER_LOAD_PROTECTION_MAX_CPU_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Worker max cpu usage, when the worker's cpu usage is smaller then this value, worker server can be dispatched tasks. | | worker.env.WORKER_SERVER_LOAD_PROTECTION_MAX_DISK_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Worker max disk usage , when the worker's disk usage is smaller then this value, worker server can be dispatched tasks. | -| worker.env.WORKER_SERVER_LOAD_PROTECTION_MAX_JVM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Worker max jvm memory usage , when the worker's jvm memory usage is smaller then this value, worker server can be dispatched tasks. | +| worker.env.WORKER_SERVER_LOAD_PROTECTION_MAX_JVM_CPU_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Worker max jvm cpu usage, when the worker's jvm cpu usage is smaller then this value, worker server can be dispatched tasks. | +| worker.env.WORKER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_CPU_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Worker max system cpu usage, when the worker's system cpu usage is smaller then this value, worker server can be dispatched tasks. | | worker.env.WORKER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS | float | `0.7` | Worker max memory usage , when the worker's memory usage is smaller then this value, worker server can be dispatched tasks. | | worker.env.WORKER_TENANT_CONFIG_AUTO_CREATE_TENANT_ENABLED | bool | `true` | tenant corresponds to the user of the system, which is used by the worker to submit the job. If system does not have this user, it will be automatically created after the parameter worker.tenant.auto.create is true. | | worker.env.WORKER_TENANT_CONFIG_DISTRIBUTED_TENANT | bool | `false` | Scenes to be used for distributed users. For example, users created by FreeIpa are stored in LDAP. This parameter only applies to Linux, When this parameter is true, worker.tenant.auto.create has no effect and will not automatically create tenants. | diff --git a/deploy/kubernetes/dolphinscheduler/values.yaml b/deploy/kubernetes/dolphinscheduler/values.yaml index a8d9a34875ca..98c2f70db07e 100644 --- a/deploy/kubernetes/dolphinscheduler/values.yaml +++ b/deploy/kubernetes/dolphinscheduler/values.yaml @@ -508,10 +508,10 @@ master: MASTER_STATE_WHEEL_INTERVAL: "5s" # -- If set true, will open master overload protection MASTER_SERVER_LOAD_PROTECTION_ENABLED: false - # -- Master max cpu usage, when the master's cpu usage is smaller then this value, master server can execute workflow. - MASTER_SERVER_LOAD_PROTECTION_MAX_CPU_USAGE_PERCENTAGE_THRESHOLDS: 0.7 - # -- Master max JVM memory usage , when the master's jvm memory usage is smaller then this value, master server can execute workflow. - MASTER_SERVER_LOAD_PROTECTION_MAX_JVM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS: 0.7 + # -- Master max system cpu usage, when the master's system cpu usage is smaller then this value, master server can execute workflow. + MASTER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_CPU_USAGE_PERCENTAGE_THRESHOLDS: 0.7 + # -- Master max jvm cpu usage, when the master's jvm cpu usage is smaller then this value, master server can execute workflow. + MASTER_SERVER_LOAD_PROTECTION_MAX_JVM_CPU_USAGE_PERCENTAGE_THRESHOLDS: 0.7 # -- Master max System memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. MASTER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS: 0.7 # -- Master max disk usage , when the master's disk usage is smaller then this value, master server can execute workflow. @@ -629,10 +629,10 @@ worker: env: # -- If set true, will open worker overload protection WORKER_SERVER_LOAD_PROTECTION_ENABLED: false - # -- Worker max cpu usage, when the worker's cpu usage is smaller then this value, worker server can be dispatched tasks. - WORKER_SERVER_LOAD_PROTECTION_MAX_CPU_USAGE_PERCENTAGE_THRESHOLDS: 0.7 - # -- Worker max jvm memory usage , when the worker's jvm memory usage is smaller then this value, worker server can be dispatched tasks. - WORKER_SERVER_LOAD_PROTECTION_MAX_JVM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS: 0.7 + # -- Worker max system cpu usage, when the worker's system cpu usage is smaller then this value, worker server can be dispatched tasks. + WORKER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_CPU_USAGE_PERCENTAGE_THRESHOLDS: 0.7 + # -- Worker max jvm cpu usage, when the worker's jvm cpu usage is smaller then this value, worker server can be dispatched tasks. + WORKER_SERVER_LOAD_PROTECTION_MAX_JVM_CPU_USAGE_PERCENTAGE_THRESHOLDS: 0.7 # -- Worker max memory usage , when the worker's memory usage is smaller then this value, worker server can be dispatched tasks. WORKER_SERVER_LOAD_PROTECTION_MAX_SYSTEM_MEMORY_USAGE_PERCENTAGE_THRESHOLDS: 0.7 # -- Worker max disk usage , when the worker's disk usage is smaller then this value, worker server can be dispatched tasks. diff --git a/docs/docs/en/architecture/configuration.md b/docs/docs/en/architecture/configuration.md index b9a26b865c77..13d89329439b 100644 --- a/docs/docs/en/architecture/configuration.md +++ b/docs/docs/en/architecture/configuration.md @@ -110,7 +110,8 @@ The directory structure of DolphinScheduler is as follows: dolphinscheduler-daemon.sh is responsible for DolphinScheduler startup and shutdown. Essentially, start-all.sh or stop-all.sh startup and shutdown the cluster via dolphinscheduler-daemon.sh. -Currently, DolphinScheduler just makes a basic config, remember to config further JVM options based on your practical situation of resources. +Currently, DolphinScheduler just makes a basic config, remember to config further JVM options based on your practical +situation of resources. Default simplified parameters are: @@ -128,44 +129,47 @@ export DOLPHINSCHEDULER_OPTS=" " ``` -> "-XX:DisableExplicitGC" is not recommended due to may lead to memory link (DolphinScheduler dependent on Netty to communicate). -> If add "-Djava.net.preferIPv6Addresses=true" will use ipv6 address, if add "-Djava.net.preferIPv4Addresses=true" will use ipv4 address, if doesn't set the two parameter will use ipv4 or ipv6. +> "-XX:DisableExplicitGC" is not recommended due to may lead to memory link (DolphinScheduler dependent on Netty to +> communicate). +> If add "-Djava.net.preferIPv6Addresses=true" will use ipv6 address, if add "-Djava.net.preferIPv4Addresses=true" will +> use ipv4 address, if doesn't set the two parameter will use ipv4 or ipv6. ### Database connection related configuration DolphinScheduler uses Spring Hikari to manage database connections, configuration file location: -|Service| Configuration file | -|--|--| -|Master Server | `master-server/conf/application.yaml`| -|Api Server| `api-server/conf/application.yaml`| -|Worker Server| `worker-server/conf/application.yaml`| -|Alert Server| `alert-server/conf/application.yaml`| +| Service | Configuration file | +|---------------|---------------------------------------| +| Master Server | `master-server/conf/application.yaml` | +| Api Server | `api-server/conf/application.yaml` | +| Worker Server | `worker-server/conf/application.yaml` | +| Alert Server | `alert-server/conf/application.yaml` | The default configuration is as follows: -|Parameters | Default value| Description| -|--|--|--| -|spring.datasource.driver-class-name| org.postgresql.Driver |datasource driver| -|spring.datasource.url| jdbc:postgresql://127.0.0.1:5432/dolphinscheduler |datasource connection url| -|spring.datasource.username|root|datasource username| -|spring.datasource.password|root|datasource password| -|spring.datasource.hikari.connection-test-query|select 1|validate connection by running the SQL| -|spring.datasource.hikari.minimum-idle| 5| minimum connection pool size number| -|spring.datasource.hikari.auto-commit|true|whether auto commit| -|spring.datasource.hikari.pool-name|DolphinScheduler|name of the connection pool| -|spring.datasource.hikari.maximum-pool-size|50| maximum connection pool size number| -|spring.datasource.hikari.connection-timeout|30000|connection timeout| -|spring.datasource.hikari.idle-timeout|600000|Maximum idle connection survival time| -|spring.datasource.hikari.leak-detection-threshold|0|Connection leak detection threshold| -|spring.datasource.hikari.initialization-fail-timeout|1|Connection pool initialization failed timeout| +| Parameters | Default value | Description | +|------------------------------------------------------|---------------------------------------------------|-----------------------------------------------| +| spring.datasource.driver-class-name | org.postgresql.Driver | datasource driver | +| spring.datasource.url | jdbc:postgresql://127.0.0.1:5432/dolphinscheduler | datasource connection url | +| spring.datasource.username | root | datasource username | +| spring.datasource.password | root | datasource password | +| spring.datasource.hikari.connection-test-query | select 1 | validate connection by running the SQL | +| spring.datasource.hikari.minimum-idle | 5 | minimum connection pool size number | +| spring.datasource.hikari.auto-commit | true | whether auto commit | +| spring.datasource.hikari.pool-name | DolphinScheduler | name of the connection pool | +| spring.datasource.hikari.maximum-pool-size | 50 | maximum connection pool size number | +| spring.datasource.hikari.connection-timeout | 30000 | connection timeout | +| spring.datasource.hikari.idle-timeout | 600000 | Maximum idle connection survival time | +| spring.datasource.hikari.leak-detection-threshold | 0 | Connection leak detection threshold | +| spring.datasource.hikari.initialization-fail-timeout | 1 | Connection pool initialization failed timeout | Note that DolphinScheduler also supports database configuration through `bin/env/dolphinscheduler_env.sh`. ### Zookeeper related configuration -DolphinScheduler uses Zookeeper for cluster management, fault tolerance, event monitoring and other functions. Configuration file location: -|Service| Configuration file | +DolphinScheduler uses Zookeeper for cluster management, fault tolerance, event monitoring and other functions. +Configuration file location: +|Service| Configuration file | |--|--| |Master Server | `master-server/conf/application.yaml`| |Api Server| `api-server/conf/application.yaml`| @@ -173,17 +177,17 @@ DolphinScheduler uses Zookeeper for cluster management, fault tolerance, event m The default configuration is as follows: -|Parameters | Default value| Description| -|--|--|--| -|registry.zookeeper.namespace|dolphinscheduler|namespace of zookeeper| -|registry.zookeeper.connect-string|localhost:2181| the connection string of zookeeper| -|registry.zookeeper.retry-policy.base-sleep-time|60ms|time to wait between subsequent retries| -|registry.zookeeper.retry-policy.max-sleep|300ms|maximum time to wait between subsequent retries| -|registry.zookeeper.retry-policy.max-retries|5|maximum retry times| -|registry.zookeeper.session-timeout|30s|session timeout| -|registry.zookeeper.connection-timeout|30s|connection timeout| -|registry.zookeeper.block-until-connected|600ms|waiting time to block until the connection succeeds| -|registry.zookeeper.digest|{username}:{password}|digest of zookeeper to access znode, works only when acl is enabled, for more details please check [https://zookeeper.apache.org/doc/r3.4.14/zookeeperAdmin.html](Apache Zookeeper doc) | +| Parameters | Default value | Description | +|-------------------------------------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| registry.zookeeper.namespace | dolphinscheduler | namespace of zookeeper | +| registry.zookeeper.connect-string | localhost:2181 | the connection string of zookeeper | +| registry.zookeeper.retry-policy.base-sleep-time | 60ms | time to wait between subsequent retries | +| registry.zookeeper.retry-policy.max-sleep | 300ms | maximum time to wait between subsequent retries | +| registry.zookeeper.retry-policy.max-retries | 5 | maximum retry times | +| registry.zookeeper.session-timeout | 30s | session timeout | +| registry.zookeeper.connection-timeout | 30s | connection timeout | +| registry.zookeeper.block-until-connected | 600ms | waiting time to block until the connection succeeds | +| registry.zookeeper.digest | {username}:{password} | digest of zookeeper to access znode, works only when acl is enabled, for more details please check [https://zookeeper.apache.org/doc/r3.4.14/zookeeperAdmin.html](Apache Zookeeper doc) | Note that DolphinScheduler also supports zookeeper related configuration through `bin/env/dolphinscheduler_env.sh`. @@ -191,12 +195,12 @@ Note that DolphinScheduler also supports zookeeper related configuration through Currently, common.properties mainly configures Hadoop,s3a related configurations. Configuration file location: -|Service| Configuration file | -|--|--| -|Master Server | `master-server/conf/common.properties`| -|Api Server| `api-server/conf/common.properties`| -|Worker Server| `worker-server/conf/common.properties`| -|Alert Server| `alert-server/conf/common.properties`| +| Service | Configuration file | +|---------------|----------------------------------------| +| Master Server | `master-server/conf/common.properties` | +| Api Server | `api-server/conf/common.properties` | +| Worker Server | `worker-server/conf/common.properties` | +| Alert Server | `alert-server/conf/common.properties` | The default configuration is as follows: @@ -237,43 +241,43 @@ The default configuration is as follows: Location: `api-server/conf/application.yaml` -|Parameters | Default value| Description| -|--|--|--| -|server.port|12345|api service communication port| -|server.servlet.session.timeout|120m|session timeout| -|server.servlet.context-path|/dolphinscheduler/ |request path| -|spring.servlet.multipart.max-file-size|1024MB|maximum file size| -|spring.servlet.multipart.max-request-size|1024MB|maximum request size| -|server.jetty.max-http-post-size|5000000|jetty maximum post size| -|spring.banner.charset|UTF-8|message encoding| -|spring.jackson.time-zone|UTC|time zone| -|spring.jackson.date-format|"yyyy-MM-dd HH:mm:ss"|time format| -|spring.messages.basename|i18n/messages|i18n config| -|security.authentication.type|PASSWORD|authentication type| -|security.authentication.ldap.user.admin|read-only-admin|admin user account when you log-in with LDAP| -|security.authentication.ldap.urls|ldap://ldap.forumsys.com:389/|LDAP urls| -|security.authentication.ldap.base.dn|dc=example,dc=com|LDAP base dn| -|security.authentication.ldap.username|cn=read-only-admin,dc=example,dc=com|LDAP username| -|security.authentication.ldap.password|password|LDAP password| -|security.authentication.ldap.user.identity-attribute|uid|LDAP user identity attribute| -|security.authentication.ldap.user.email-attribute|mail|LDAP user email attribute| -|security.authentication.ldap.user.not-exist-action|CREATE|action when ldap user is not exist,default value: CREATE. Optional values include(CREATE,DENY)| -|security.authentication.ldap.ssl.enable|false|LDAP ssl switch| -|security.authentication.ldap.ssl.trust-store|ldapkeystore.jks|LDAP jks file absolute path| -|security.authentication.ldap.ssl.trust-store-password|password|LDAP jks password| -|security.authentication.casdoor.user.admin||admin user account when you log-in with Casdoor| -|casdoor.endpoint||Casdoor server url| -|casdoor.client-id||id in Casdoor| -|casdoor.client-secret||secret in Casdoor| -|casdoor.certificate||certificate in Casdoor| -|casdoor.organization-name||organization name in Casdoor| -|casdoor.application-name||application name in Casdoor| -|casdoor.redirect-url||doplhinscheduler login url| -|api.traffic.control.global.switch|false|traffic control global switch| -|api.traffic.control.max-global-qps-rate|300|global max request number per second| -|api.traffic.control.tenant-switch|false|traffic control tenant switch| -|api.traffic.control.default-tenant-qps-rate|10|default tenant max request number per second| -|api.traffic.control.customize-tenant-qps-rate||customize tenant max request number per second| +| Parameters | Default value | Description | +|-------------------------------------------------------|--------------------------------------|------------------------------------------------------------------------------------------------| +| server.port | 12345 | api service communication port | +| server.servlet.session.timeout | 120m | session timeout | +| server.servlet.context-path | /dolphinscheduler/ | request path | +| spring.servlet.multipart.max-file-size | 1024MB | maximum file size | +| spring.servlet.multipart.max-request-size | 1024MB | maximum request size | +| server.jetty.max-http-post-size | 5000000 | jetty maximum post size | +| spring.banner.charset | UTF-8 | message encoding | +| spring.jackson.time-zone | UTC | time zone | +| spring.jackson.date-format | "yyyy-MM-dd HH:mm:ss" | time format | +| spring.messages.basename | i18n/messages | i18n config | +| security.authentication.type | PASSWORD | authentication type | +| security.authentication.ldap.user.admin | read-only-admin | admin user account when you log-in with LDAP | +| security.authentication.ldap.urls | ldap://ldap.forumsys.com:389/ | LDAP urls | +| security.authentication.ldap.base.dn | dc=example,dc=com | LDAP base dn | +| security.authentication.ldap.username | cn=read-only-admin,dc=example,dc=com | LDAP username | +| security.authentication.ldap.password | password | LDAP password | +| security.authentication.ldap.user.identity-attribute | uid | LDAP user identity attribute | +| security.authentication.ldap.user.email-attribute | mail | LDAP user email attribute | +| security.authentication.ldap.user.not-exist-action | CREATE | action when ldap user is not exist,default value: CREATE. Optional values include(CREATE,DENY) | +| security.authentication.ldap.ssl.enable | false | LDAP ssl switch | +| security.authentication.ldap.ssl.trust-store | ldapkeystore.jks | LDAP jks file absolute path | +| security.authentication.ldap.ssl.trust-store-password | password | LDAP jks password | +| security.authentication.casdoor.user.admin | | admin user account when you log-in with Casdoor | +| casdoor.endpoint | | Casdoor server url | +| casdoor.client-id | | id in Casdoor | +| casdoor.client-secret | | secret in Casdoor | +| casdoor.certificate | | certificate in Casdoor | +| casdoor.organization-name | | organization name in Casdoor | +| casdoor.application-name | | application name in Casdoor | +| casdoor.redirect-url | | doplhinscheduler login url | +| api.traffic.control.global.switch | false | traffic control global switch | +| api.traffic.control.max-global-qps-rate | 300 | global max request number per second | +| api.traffic.control.tenant-switch | false | traffic control tenant switch | +| api.traffic.control.default-tenant-qps-rate | 10 | default tenant max request number per second | +| api.traffic.control.customize-tenant-qps-rate | | customize tenant max request number per second | ### Master Server related configuration @@ -292,9 +296,9 @@ Location: `master-server/conf/application.yaml` | master.task-commit-interval | 1000 | master commit task interval, the unit is millisecond | | master.state-wheel-interval | 5 | time to check status | | master.server-load-protection.enabled | true | If set true, will open master overload protection | -| master.server-load-protection.max-cpu-usage-percentage-thresholds | 0.7 | Master max cpu usage, when the master's cpu usage is smaller then this value, master server can execute workflow. | -| master.server-load-protection.max-jvm-memory-usage-percentage-thresholds | 0.7 | Master max JVM memory usage , when the master's jvm memory usage is smaller then this value, master server can execute workflow. | -| master.server-load-protection.max-system-memory-usage-percentage-thresholds | 0.7 | Master max System memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. | +| master.server-load-protection.max-system-cpu-usage-percentage-thresholds | 0.7 | Master max system cpu usage, when the master's system cpu usage is smaller then this value, master server can execute workflow. | +| master.server-load-protection.max-jvm-cpu-usage-percentage-thresholds | 0.7 | Master max JVM cpu usage, when the master's jvm cpu usage is smaller then this value, master server can execute workflow. | +| master.server-load-protection.max-system-memory-usage-percentage-thresholds | 0.7 | Master max system memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. | | master.server-load-protection.max-disk-usage-percentage-thresholds | 0.7 | Master max disk usage , when the master's disk usage is smaller then this value, master server can execute workflow. | | master.failover-interval | 10 | failover interval, the unit is minute | | master.kill-application-when-task-failover | true | whether to kill yarn/k8s application when failover taskInstance | @@ -306,23 +310,23 @@ Location: `master-server/conf/application.yaml` Location: `worker-server/conf/application.yaml` -| Parameters | Default value | Description | -|--------------------------------------------------------------------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| worker.listen-port | 1234 | worker-service listen port | -| worker.exec-threads | 100 | worker-service execute thread number, used to limit the number of task instances in parallel | -| worker.max-heartbeat-interval | 10s | worker-service max heartbeat interval | -| worker.host-weight | 100 | worker host weight to dispatch tasks | -| worker.server-load-protection.enabled | true | If set true will open worker overload protection | -| worker.max-cpu-usage-percentage-thresholds.max-cpu-usage-percentage-thresholds | 0.7 | Master max cpu usage, when the master's cpu usage is smaller then this value, master server can execute workflow. | -| worker.server-load-protection.max-jvm-memory-usage-percentage-thresholds | 0.7 | Master max JVM memory usage , when the master's jvm memory usage is smaller then this value, master server can execute workflow. | -| worker.server-load-protection.max-system-memory-usage-percentage-thresholds | 0.7 | Master max System memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. | -| worker.server-load-protection.max-disk-usage-percentage-thresholds | 0.7 | Master max disk usage , when the master's disk usage is smaller then this value, master server can execute workflow. | -| worker.registry-disconnect-strategy.strategy | stop | Used when the worker disconnect from registry, default value: stop. Optional values include stop, waiting | -| worker.registry-disconnect-strategy.max-waiting-time | 100s | Used when the worker disconnect from registry, and the disconnect strategy is waiting, this config means the worker will waiting to reconnect to registry in given times, and after the waiting times, if the worker still cannot connect to registry, will stop itself, if the value is 0s, will wait infinitely | -| worker.task-execute-threads-full-policy | REJECT | If REJECT, when the task waiting in the worker reaches exec-threads, it will reject the received task and the Master will redispatch it; If CONTINUE, it will put the task into the worker's execution queue and wait for a free thread to start execution | -| worker.tenant-config.auto-create-tenant-enabled | true | tenant corresponds to the user of the system, which is used by the worker to submit the job. If system does not have this user, it will be automatically created after the parameter worker.tenant.auto.create is true. | -| worker.tenant-config.distributed-tenant-enabled | false | When this parameter is true, auto-create-tenant-enabled has no effect and will not automatically create tenants | -| worker.tenant-config.default-tenant-enabled | false | If set true, will use worker bootstrap user as the tenant to execute task when the tenant is `default`. | +| Parameters | Default value | Description | +|-----------------------------------------------------------------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| worker.listen-port | 1234 | worker-service listen port | +| worker.exec-threads | 100 | worker-service execute thread number, used to limit the number of task instances in parallel | +| worker.max-heartbeat-interval | 10s | worker-service max heartbeat interval | +| worker.host-weight | 100 | worker host weight to dispatch tasks | +| worker.server-load-protection.enabled | true | If set true will open worker overload protection | +| worker.server-load-protection.max-system-cpu-usage-percentage-thresholds | 0.7 | Worker max system cpu usage, when the worker's system cpu usage is smaller then this value, master server can execute workflow. | +| worker.server-load-protection.max-jvm-cpu-usage-percentage-thresholds | 0.7 | Worker max JVM cpu usage, when the worker's jvm cpu usage is smaller then this value, master server can execute workflow. | +| worker.server-load-protection.max-system-memory-usage-percentage-thresholds | 0.7 | Worker max system memory usage , when the worker's system memory usage is smaller then this value, master server can execute workflow. | +| worker.server-load-protection.max-disk-usage-percentage-thresholds | 0.7 | Worker max disk usage , when the worker's disk usage is smaller then this value, master server can execute workflow. | +| worker.registry-disconnect-strategy.strategy | stop | Used when the worker disconnect from registry, default value: stop. Optional values include stop, waiting | +| worker.registry-disconnect-strategy.max-waiting-time | 100s | Used when the worker disconnect from registry, and the disconnect strategy is waiting, this config means the worker will waiting to reconnect to registry in given times, and after the waiting times, if the worker still cannot connect to registry, will stop itself, if the value is 0s, will wait infinitely | +| worker.task-execute-threads-full-policy | REJECT | If REJECT, when the task waiting in the worker reaches exec-threads, it will reject the received task and the Master will redispatch it; If CONTINUE, it will put the task into the worker's execution queue and wait for a free thread to start execution | +| worker.tenant-config.auto-create-tenant-enabled | true | tenant corresponds to the user of the system, which is used by the worker to submit the job. If system does not have this user, it will be automatically created after the parameter worker.tenant.auto.create is true. | +| worker.tenant-config.distributed-tenant-enabled | false | When this parameter is true, auto-create-tenant-enabled has no effect and will not automatically create tenants | +| worker.tenant-config.default-tenant-enabled | false | If set true, will use worker bootstrap user as the tenant to execute task when the tenant is `default`. | ### Alert Server related configuration @@ -337,10 +341,10 @@ Location: `alert-server/conf/application.yaml` This part describes quartz configs and configure them based on your practical situation and resources. -|Service| Configuration file | -|--|--| -|Master Server | `master-server/conf/application.yaml`| -|Api Server| `api-server/conf/application.yaml`| +| Service | Configuration file | +|---------------|---------------------------------------| +| Master Server | `master-server/conf/application.yaml` | +| Api Server | `api-server/conf/application.yaml` | The default configuration is as follows: @@ -358,7 +362,8 @@ The default configuration is as follows: | spring.quartz.properties.org.quartz.jobStore.driverDelegateClass | org.quartz.impl.jdbcjobstore.PostgreSQLDelegate | | spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval | 5000 | -The above configuration items is the same in *Master Server* and *Api Server*, but their *Quartz Scheduler* threadpool configuration is different. +The above configuration items is the same in *Master Server* and *Api Server*, but their *Quartz Scheduler* threadpool +configuration is different. The default quartz threadpool configuration in *Master Server* is as follows: @@ -369,7 +374,8 @@ The default quartz threadpool configuration in *Master Server* is as follows: | spring.quartz.properties.org.quartz.threadPool.threadPriority | 5 | | spring.quartz.properties.org.quartz.threadPool.class | org.quartz.simpl.SimpleThreadPool | -Since *Api Server* will not start *Quartz Scheduler* instance, as a client only, therefore it's threadpool is configured as `QuartzZeroSizeThreadPool` which has zero thread; +Since *Api Server* will not start *Quartz Scheduler* instance, as a client only, therefore it's threadpool is configured +as `QuartzZeroSizeThreadPool` which has zero thread; The default configuration is as follows: | Parameters | Default value | @@ -378,7 +384,8 @@ The default configuration is as follows: ### dolphinscheduler_env.sh [load environment variables configs] -When using shell to commit tasks, DolphinScheduler will export environment variables from `bin/env/dolphinscheduler_env.sh`. The +When using shell to commit tasks, DolphinScheduler will export environment variables +from `bin/env/dolphinscheduler_env.sh`. The mainly configuration including `JAVA_HOME` and other environment paths. ```bash @@ -406,9 +413,10 @@ export FLINK_ENV_JAVA_OPTS="-javaagent:${DOLPHINSCHEDULER_HOME}/tools/libs/aspec ### Log related configuration -|Service| Configuration file | -|--|--| -|Master Server | `master-server/conf/logback-spring.xml`| -|Api Server| `api-server/conf/logback-spring.xml`| -|Worker Server| `worker-server/conf/logback-spring.xml`| -|Alert Server| `alert-server/conf/logback-spring.xml`| +| Service | Configuration file | +|---------------|-----------------------------------------| +| Master Server | `master-server/conf/logback-spring.xml` | +| Api Server | `api-server/conf/logback-spring.xml` | +| Worker Server | `worker-server/conf/logback-spring.xml` | +| Alert Server | `alert-server/conf/logback-spring.xml` | + diff --git a/docs/docs/zh/architecture/configuration.md b/docs/docs/zh/architecture/configuration.md index 0b3ea9bc5bd1..08fded19e069 100644 --- a/docs/docs/zh/architecture/configuration.md +++ b/docs/docs/zh/architecture/configuration.md @@ -130,38 +130,40 @@ export DOLPHINSCHEDULER_OPTS=" > 不建议设置"-XX:DisableExplicitGC" , DolphinScheduler使用Netty进行通讯,设置该参数,可能会导致内存泄漏. > ->> 如果设置"-Djava.net.preferIPv6Addresses=true" 将会使用ipv6的IP地址, 如果设置"-Djava.net.preferIPv4Addresses=true"将会使用ipv4的IP地址, 如果都不设置,将会随机使用ipv4或者ipv6. +>> 如果设置"-Djava.net.preferIPv6Addresses=true" 将会使用ipv6的IP地址, 如果设置"-Djava.net.preferIPv4Addresses=true" +>> 将会使用ipv4的IP地址, 如果都不设置,将会随机使用ipv4或者ipv6. ## 数据库连接相关配置 在DolphinScheduler中使用Spring Hikari对数据库连接进行管理,配置文件位置: -|服务名称| 配置文件 | -|--|--| -|Master Server | `master-server/conf/application.yaml`| -|Api Server| `api-server/conf/application.yaml`| -|Worker Server| `worker-server/conf/application.yaml`| -|Alert Server| `alert-server/conf/application.yaml`| +| 服务名称 | 配置文件 | +|---------------|---------------------------------------| +| Master Server | `master-server/conf/application.yaml` | +| Api Server | `api-server/conf/application.yaml` | +| Worker Server | `worker-server/conf/application.yaml` | +| Alert Server | `alert-server/conf/application.yaml` | 默认配置如下: -|参数 | 默认值| 描述| -|--|--|--| -|spring.datasource.driver-class-name| org.postgresql.Driver |数据库驱动| -|spring.datasource.url| jdbc:postgresql://127.0.0.1:5432/dolphinscheduler |数据库连接地址| -|spring.datasource.username|root|数据库用户名| -|spring.datasource.password|root|数据库密码| -|spring.datasource.hikari.connection-test-query|select 1|检测连接是否有效的sql| -|spring.datasource.hikari.minimum-idle| 5|最小空闲连接池数量| -|spring.datasource.hikari.auto-commit|true|是否自动提交| -|spring.datasource.hikari.pool-name|DolphinScheduler|连接池名称| -|spring.datasource.hikari.maximum-pool-size|50|连接池最大连接数| -|spring.datasource.hikari.connection-timeout|30000|连接超时时长| -|spring.datasource.hikari.idle-timeout|600000|空闲连接存活最大时间| -|spring.datasource.hikari.leak-detection-threshold|0|连接泄露检测阈值| -|spring.datasource.hikari.initialization-fail-timeout|1|连接池初始化失败timeout| - -DolphinScheduler同样可以通过设置环境变量进行数据库连接相关的配置, 将以上小写字母转成大写并把`.`换成`_`作为环境变量名, 设置值即可。 +| 参数 | 默认值 | 描述 | +|------------------------------------------------------|---------------------------------------------------|-----------------| +| spring.datasource.driver-class-name | org.postgresql.Driver | 数据库驱动 | +| spring.datasource.url | jdbc:postgresql://127.0.0.1:5432/dolphinscheduler | 数据库连接地址 | +| spring.datasource.username | root | 数据库用户名 | +| spring.datasource.password | root | 数据库密码 | +| spring.datasource.hikari.connection-test-query | select 1 | 检测连接是否有效的sql | +| spring.datasource.hikari.minimum-idle | 5 | 最小空闲连接池数量 | +| spring.datasource.hikari.auto-commit | true | 是否自动提交 | +| spring.datasource.hikari.pool-name | DolphinScheduler | 连接池名称 | +| spring.datasource.hikari.maximum-pool-size | 50 | 连接池最大连接数 | +| spring.datasource.hikari.connection-timeout | 30000 | 连接超时时长 | +| spring.datasource.hikari.idle-timeout | 600000 | 空闲连接存活最大时间 | +| spring.datasource.hikari.leak-detection-threshold | 0 | 连接泄露检测阈值 | +| spring.datasource.hikari.initialization-fail-timeout | 1 | 连接池初始化失败timeout | + +DolphinScheduler同样可以通过设置环境变量进行数据库连接相关的配置, 将以上小写字母转成大写并把`.`换成`_`作为环境变量名, +设置值即可。 ## Zookeeper相关配置 @@ -174,17 +176,17 @@ DolphinScheduler使用Zookeeper进行集群管理、容错、事件监听等功 默认配置如下: -|参数 |默认值| 描述| -|--|--|--| -|registry.zookeeper.namespace|dolphinscheduler|Zookeeper集群使用的namespace| -|registry.zookeeper.connect-string|localhost:2181| Zookeeper集群连接信息| -|registry.zookeeper.retry-policy.base-sleep-time|60ms|基本重试时间差| -|registry.zookeeper.retry-policy.max-sleep|300ms|最大重试时间| -|registry.zookeeper.retry-policy.max-retries|5|最大重试次数| -|registry.zookeeper.session-timeout|30s|session超时时间| -|registry.zookeeper.connection-timeout|30s|连接超时时间| -|registry.zookeeper.block-until-connected|600ms|阻塞直到连接成功的等待时间| -|registry.zookeeper.digest|{用户名:密码}|如果zookeeper打开了acl,则需要填写认证信息访问znode,认证信息格式为{用户名}:{密码}。关于Zookeeper ACL详见[https://zookeeper.apache.org/doc/r3.4.14/zookeeperAdmin.html](Apache Zookeeper官方文档)| +| 参数 | 默认值 | 描述 | +|-------------------------------------------------|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| registry.zookeeper.namespace | dolphinscheduler | Zookeeper集群使用的namespace | +| registry.zookeeper.connect-string | localhost:2181 | Zookeeper集群连接信息 | +| registry.zookeeper.retry-policy.base-sleep-time | 60ms | 基本重试时间差 | +| registry.zookeeper.retry-policy.max-sleep | 300ms | 最大重试时间 | +| registry.zookeeper.retry-policy.max-retries | 5 | 最大重试次数 | +| registry.zookeeper.session-timeout | 30s | session超时时间 | +| registry.zookeeper.connection-timeout | 30s | 连接超时时间 | +| registry.zookeeper.block-until-connected | 600ms | 阻塞直到连接成功的等待时间 | +| registry.zookeeper.digest | {用户名:密码} | 如果zookeeper打开了acl,则需要填写认证信息访问znode,认证信息格式为{用户名}:{密码}。关于Zookeeper ACL详见[https://zookeeper.apache.org/doc/r3.4.14/zookeeperAdmin.html](Apache Zookeeper官方文档) | DolphinScheduler同样可以通过`bin/env/dolphinscheduler_env.sh`进行Zookeeper相关的配置。 @@ -200,8 +202,8 @@ common.properties配置文件目前主要是配置hadoop/s3/yarn/applicationId 默认配置如下: -| 参数 | 默认值 | 描述 | -|-----------------------------------------------|--------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 参数 | 默认值 | 描述 | +|-----------------------------------------------|--------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | data.basedir.path | /tmp/dolphinscheduler | 本地工作目录,用于存放临时文件 | | resource.storage.type | NONE | 资源文件存储类型: HDFS,S3,OSS,GCS,ABS,NONE | | resource.upload.path | /dolphinscheduler | 资源文件存储路径 | @@ -279,48 +281,54 @@ common.properties配置文件目前主要是配置hadoop/s3/yarn/applicationId 位置:`master-server/conf/application.yaml` -| 参数 | 默认值 | 描述 | -|--------------------------------------------------------|--------------|-----------------------------------------------------------------------------------| -| master.listen-port | 5678 | master监听端口 | -| master.fetch-command-num | 10 | master拉取command数量 | -| master.pre-exec-threads | 10 | master准备执行任务的数量,用于限制并行的command | -| master.exec-threads | 100 | master工作线程数量,用于限制并行的流程实例数量 | -| master.dispatch-task-number | 3 | master每个批次的派发任务数量 | -| master.host-selector | lower_weight | master host选择器,用于选择合适的worker执行任务,可选值: random, round_robin, lower_weight | -| master.max-heartbeat-interval | 10s | master最大心跳间隔 | -| master.task-commit-retry-times | 5 | 任务重试次数 | -| master.task-commit-interval | 1000 | 任务提交间隔,单位为毫秒 | -| master.state-wheel-interval | 5 | 轮询检查状态时间 | -| master.max-cpu-load-avg | 1 | master最大cpuload均值,只有高于系统cpuload均值时,master服务才能调度任务. 默认值为1: 会使用100%的CPU | -| master.reserved-memory | 0.3 | master预留内存,只有低于系统可用内存时,master服务才能调度任务. 默认值为0.3:当系统内存低于30%时会停止调度新的工作流 | -| master.failover-interval | 10 | failover间隔,单位为分钟 | -| master.kill-application-when-task-failover | true | 当任务实例failover时,是否kill掉yarn或k8s application | -| master.registry-disconnect-strategy.strategy | stop | 当Master与注册中心失联之后采取的策略, 默认值是: stop. 可选值包括: stop, waiting | -| master.registry-disconnect-strategy.max-waiting-time | 100s | 当Master与注册中心失联之后重连时间, 之后当strategy为waiting时,该值生效。 该值表示当Master与注册中心失联时会在给定时间之内进行重连, | -| 在给定时间之内重连失败将会停止自己,在重连时,Master会丢弃目前正在执行的工作流,值为0表示会无限期等待 | -| master.master.worker-group-refresh-interval | 10s | 定期将workerGroup从数据库中同步到内存的时间间隔 | +| 参数 | 默认值 | 描述 | +|-----------------------------------------------------------------------------|--------------|-----------------------------------------------------------------------------------------| +| master.listen-port | 5678 | master监听端口 | +| master.fetch-command-num | 10 | master拉取command数量 | +| master.pre-exec-threads | 10 | master准备执行任务的数量,用于限制并行的command | +| master.exec-threads | 100 | master工作线程数量,用于限制并行的流程实例数量 | +| master.dispatch-task-number | 3 | master每个批次的派发任务数量 | +| master.host-selector | lower_weight | master host选择器,用于选择合适的worker执行任务,可选值: random, round_robin, lower_weight | +| master.max-heartbeat-interval | 10s | master最大心跳间隔 | +| master.task-commit-retry-times | 5 | 任务重试次数 | +| master.task-commit-interval | 1000 | 任务提交间隔,单位为毫秒 | +| master.state-wheel-interval | 5 | 轮询检查状态时间 | +| master.server-load-protection.enabled | true | 是否开启系统保护策略 | +| master.server-load-protection.max-system-cpu-usage-percentage-thresholds | 0.7 | master最大系统cpu使用值,只有当前系统cpu使用值低于最大系统cpu使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统CPU | +| master.server-load-protection.max-jvm-cpu-usage-percentage-thresholds | 0.7 | master最大JVM cpu使用值,只有当前JVM cpu使用值低于最大JVM cpu使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的JVM CPU | +| master.server-load-protection.max-system-memory-usage-percentage-thresholds | 0.7 | master最大系统 内存使用值,只有当前系统内存使用值低于最大系统内存使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统内存 | +| master.server-load-protection.max-disk-usage-percentage-thresholds | 0.7 | master最大系统磁盘使用值,只有当前系统磁盘使用值低于最大系统磁盘使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统磁盘空间 | +| master.failover-interval | 10 | failover间隔,单位为分钟 | +| master.kill-application-when-task-failover | true | 当任务实例failover时,是否kill掉yarn或k8s application | +| master.registry-disconnect-strategy.strategy | stop | 当Master与注册中心失联之后采取的策略, 默认值是: stop. 可选值包括: stop, waiting | +| master.registry-disconnect-strategy.max-waiting-time | 100s | 当Master与注册中心失联之后重连时间, 之后当strategy为waiting时,该值生效。 该值表示当Master与注册中心失联时会在给定时间之内进行重连, | +| 在给定时间之内重连失败将会停止自己,在重连时,Master会丢弃目前正在执行的工作流,值为0表示会无限期等待 | +| master.master.worker-group-refresh-interval | 10s | 定期将workerGroup从数据库中同步到内存的时间间隔 | ## Worker Server相关配置 位置:`worker-server/conf/application.yaml` -| 参数 | 默认值 | 描述 | -|------------------------------------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------| -| worker.listen-port | 1234 | worker监听端口 | -| worker.exec-threads | 100 | worker工作线程数量,用于限制并行的任务实例数量 | -| worker.max-heartbeat-interval | 10s | worker最大心跳间隔 | -| worker.host-weight | 100 | 派发任务时,worker主机的权重 | -| worker.tenant-auto-create | true | 租户对应于系统的用户,由worker提交作业.如果系统没有该用户,则在参数worker.tenant.auto.create为true后自动创建。 | -| worker.max-cpu-load-avg | 1 | worker最大cpuload均值,只有高于系统cpuload均值时,worker服务才能被派发任务. 默认值为1: 会使用100%的CPU | -| worker.reserved-memory | 0.3 | worker预留内存,只有低于系统可用内存时,worker服务才能被派发任务. 默认值为0.3:当系统内存低于30%时会停止调度新的工作流 | -| worker.alert-listen-host | localhost | alert监听host | -| worker.alert-listen-port | 50052 | alert监听端口 | -| worker.registry-disconnect-strategy.strategy | stop | 当Worker与注册中心失联之后采取的策略, 默认值是: stop. 可选值包括: stop, waiting | -| worker.registry-disconnect-strategy.max-waiting-time | 100s | 当Worker与注册中心失联之后重连时间, 之后当strategy为waiting时,该值生效。 该值表示当Worker与注册中心失联时会在给定时间之内进行重连, 在给定时间之内重连失败将会停止自己,在重连时,Worker会丢弃kill正在执行的任务。值为0表示会无限期等待 | -| worker.task-execute-threads-full-policy | REJECT | 如果是 REJECT, 当Worker中等待队列中的任务数达到exec-threads时, Worker将会拒绝接下来新接收的任务,Master将会重新分发该任务; 如果是 CONTINUE, Worker将会接收任务,放入等待队列中等待空闲线程去执行该任务 | -| worker.tenant-config.auto-create-tenant-enabled | true | 租户对应于系统的用户,由worker提交作业.如果系统没有该用户,则在参数worker.tenant.auto.create为true后自动创建。 | -| worker.tenant-config.distributed-tenant-enabled | false | 如果设置为true, auto-create-tenant-enabled 将会不起作用。 | -| worker.tenant-config.default-tenant-enabled | false | 如果设置为true, 将会使用worker服务启动用户作为 `default` 租户。 | +| 参数 | 默认值 | 描述 | +|-----------------------------------------------------------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------| +| worker.listen-port | 1234 | worker监听端口 | +| worker.exec-threads | 100 | worker工作线程数量,用于限制并行的任务实例数量 | +| worker.max-heartbeat-interval | 10s | worker最大心跳间隔 | +| worker.host-weight | 100 | 派发任务时,worker主机的权重 | +| worker.tenant-auto-create | true | 租户对应于系统的用户,由worker提交作业.如果系统没有该用户,则在参数worker.tenant.auto.create为true后自动创建。 | +| worker.server-load-protection.enabled | true | 是否开启系统保护策略 | +| worker.server-load-protection.max-system-cpu-usage-percentage-thresholds | 0.7 | worker最大系统cpu使用值,只有当前系统cpu使用值低于最大系统cpu使用值,worker服务才能接收任务. 默认值为0.7: 会使用70%的操作系统CPU | +| worker.server-load-protection.max-jvm-cpu-usage-percentage-thresholds | 0.7 | worker最大JVM cpu使用值,只有当前JVM cpu使用值低于最大JVM cpu使用值,worker服务才能接收任务. 默认值为0.7: 会使用70%的JVM CPU | +| worker.server-load-protection.max-system-memory-usage-percentage-thresholds | 0.7 | worker最大系统 内存使用值,只有当前系统内存使用值低于最大系统内存使用值,worker服务才能接收任务. 默认值为0.7: 会使用70%的操作系统内存 | +| worker.server-load-protection.max-disk-usage-percentage-thresholds | 0.7 | worker最大系统磁盘使用值,只有当前系统磁盘使用值低于最大系统磁盘使用值,worker服务才能接收任务. 默认值为0.7: 会使用70%的操作系统磁盘空间 | +| worker.alert-listen-host | localhost | alert监听host | +| worker.alert-listen-port | 50052 | alert监听端口 | +| worker.registry-disconnect-strategy.strategy | stop | 当Worker与注册中心失联之后采取的策略, 默认值是: stop. 可选值包括: stop, waiting | +| worker.registry-disconnect-strategy.max-waiting-time | 100s | 当Worker与注册中心失联之后重连时间, 之后当strategy为waiting时,该值生效。 该值表示当Worker与注册中心失联时会在给定时间之内进行重连, 在给定时间之内重连失败将会停止自己,在重连时,Worker会丢弃kill正在执行的任务。值为0表示会无限期等待 | +| worker.task-execute-threads-full-policy | REJECT | 如果是 REJECT, 当Worker中等待队列中的任务数达到exec-threads时, Worker将会拒绝接下来新接收的任务,Master将会重新分发该任务; 如果是 CONTINUE, Worker将会接收任务,放入等待队列中等待空闲线程去执行该任务 | +| worker.tenant-config.auto-create-tenant-enabled | true | 租户对应于系统的用户,由worker提交作业.如果系统没有该用户,则在参数worker.tenant.auto.create为true后自动创建。 | +| worker.tenant-config.distributed-tenant-enabled | false | 如果设置为true, auto-create-tenant-enabled 将会不起作用。 | +| worker.tenant-config.default-tenant-enabled | false | 如果设置为true, 将会使用worker服务启动用户作为 `default` 租户。 | ## Alert Server相关配置 @@ -366,7 +374,9 @@ common.properties配置文件目前主要是配置hadoop/s3/yarn/applicationId | spring.quartz.properties.org.quartz.threadPool.threadPriority | 5 | | spring.quartz.properties.org.quartz.threadPool.class | org.quartz.simpl.SimpleThreadPool | -因为*Api Server*不会启动*Quartz Scheduler*实例,只会作为Scheduler客户端使用,因此它的Quartz线程池将会使用`QuartzZeroSizeThreadPool`。`QuartzZeroSizeThreadPool`不会启动任何线程。具体的默认配置如下: +因为*Api Server*不会启动*Quartz Scheduler* +实例,只会作为Scheduler客户端使用,因此它的Quartz线程池将会使用`QuartzZeroSizeThreadPool`。`QuartzZeroSizeThreadPool` +不会启动任何线程。具体的默认配置如下: | Parameters | Default value | |------------------------------------------------------|-----------------------------------------------------------------------| @@ -374,7 +384,8 @@ common.properties配置文件目前主要是配置hadoop/s3/yarn/applicationId ## dolphinscheduler_env.sh [环境变量配置] -通过类似shell方式提交任务的的时候,会加载该配置文件中的环境变量到主机中。涉及到的 `JAVA_HOME` 任务类型的环境配置,其中任务类型主要有: Shell任务、Python任务、Spark任务、Flink任务、Datax任务等等。 +通过类似shell方式提交任务的的时候,会加载该配置文件中的环境变量到主机中。涉及到的 `JAVA_HOME` +任务类型的环境配置,其中任务类型主要有: Shell任务、Python任务、Spark任务、Flink任务、Datax任务等等。 ```bash # JAVA_HOME, will use it to start DolphinScheduler server @@ -401,9 +412,10 @@ export FLINK_ENV_JAVA_OPTS="-javaagent:${DOLPHINSCHEDULER_HOME}/tools/libs/aspec ## 日志相关配置 -|服务名称| 配置文件 | -|--|--| -|Master Server | `master-server/conf/logback-spring.xml`| -|Api Server| `api-server/conf/logback-spring.xml`| -|Worker Server| `worker-server/conf/logback-spring.xml`| -|Alert Server| `alert-server/conf/logback-spring.xml`| +| 服务名称 | 配置文件 | +|---------------|-----------------------------------------| +| Master Server | `master-server/conf/logback-spring.xml` | +| Api Server | `api-server/conf/logback-spring.xml` | +| Worker Server | `worker-server/conf/logback-spring.xml` | +| Alert Server | `alert-server/conf/logback-spring.xml` | + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertHeartbeatTask.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertHeartbeatTask.java index 3b2d588f8554..0bfefed223f3 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertHeartbeatTask.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertHeartbeatTask.java @@ -65,7 +65,8 @@ public AlertServerHeartBeat getHeartBeat() { .processId(processId) .startupTime(startupTime) .reportTime(System.currentTimeMillis()) - .cpuUsage(systemMetrics.getTotalCpuUsedPercentage()) + .jvmCpuUsage(systemMetrics.getJvmCpuUsagePercentage()) + .cpuUsage(systemMetrics.getSystemCpuUsagePercentage()) .memoryUsage(systemMetrics.getSystemMemoryUsedPercentage()) .jvmMemoryUsage(systemMetrics.getJvmMemoryUsedPercentage()) .serverStatus(ServerStatus.NORMAL) diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/AlertServerHeartBeat.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/AlertServerHeartBeat.java index 7cbd83b8ce0d..9faaef82be4f 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/AlertServerHeartBeat.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/AlertServerHeartBeat.java @@ -17,33 +17,11 @@ package org.apache.dolphinscheduler.common.model; -import org.apache.dolphinscheduler.common.enums.ServerStatus; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; -@Data -@Builder +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor -public class AlertServerHeartBeat implements HeartBeat { - - private int processId; - private long startupTime; - private long reportTime; - private double cpuUsage; - private double memoryUsage; - private double jvmMemoryUsage; - - private ServerStatus serverStatus; - - private String host; - private int port; +public class AlertServerHeartBeat extends BaseHeartBeat implements HeartBeat { - @Override - public ServerStatus getServerStatus() { - return serverStatus; - } } diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/BaseHeartBeat.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/BaseHeartBeat.java new file mode 100644 index 000000000000..2837e5482b76 --- /dev/null +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/BaseHeartBeat.java @@ -0,0 +1,46 @@ +/* + * 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.common.model; + +import org.apache.dolphinscheduler.common.enums.ServerStatus; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class BaseHeartBeat implements HeartBeat { + + protected int processId; + protected long startupTime; + protected long reportTime; + protected double jvmCpuUsage; + protected double cpuUsage; + protected double jvmMemoryUsage; + protected double memoryUsage; + protected double diskUsage; + protected ServerStatus serverStatus; + + protected String host; + protected int port; + +} diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/HeartBeat.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/HeartBeat.java index 3a105227aa08..35971b398b6e 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/HeartBeat.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/HeartBeat.java @@ -21,10 +21,6 @@ public interface HeartBeat { - String getHost(); - ServerStatus getServerStatus(); - int getPort(); - } diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/MasterHeartBeat.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/MasterHeartBeat.java index ecc140bcfb5b..b8ae4512dd6d 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/MasterHeartBeat.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/MasterHeartBeat.java @@ -17,33 +17,11 @@ package org.apache.dolphinscheduler.common.model; -import org.apache.dolphinscheduler.common.enums.ServerStatus; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; -@Data -@Builder +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor -public class MasterHeartBeat implements HeartBeat { - - private long startupTime; - private long reportTime; - private double cpuUsage; - private double jvmMemoryUsage; - private double memoryUsage; - private double diskUsage; - private ServerStatus serverStatus; - private int processId; - - private String host; - private int port; +public class MasterHeartBeat extends BaseHeartBeat implements HeartBeat { - @Override - public ServerStatus getServerStatus() { - return serverStatus; - } } diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/WorkerHeartBeat.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/WorkerHeartBeat.java index 056fc6a2c713..c02748619818 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/WorkerHeartBeat.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/WorkerHeartBeat.java @@ -17,37 +17,18 @@ package org.apache.dolphinscheduler.common.model; -import org.apache.dolphinscheduler.common.enums.ServerStatus; - -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data -@Builder +@EqualsAndHashCode(callSuper = true) +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor -public class WorkerHeartBeat implements HeartBeat { - - private long startupTime; - private long reportTime; - private double cpuUsage; - private double jvmMemoryUsage; - private double memoryUsage; - private double diskUsage; - private ServerStatus serverStatus; - private int processId; - - private String host; - private int port; +public class WorkerHeartBeat extends BaseHeartBeat implements HeartBeat { private int workerHostWeight; // worker host weight private int threadPoolUsage; // worker waiting task count - @Override - public ServerStatus getServerStatus() { - return serverStatus; - } - } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java index d37ca9d0167f..752479e60086 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java @@ -123,7 +123,7 @@ public void run() throws SchedulerException { MasterServerMetrics.registerMasterCpuUsageGauge(() -> { SystemMetrics systemMetrics = metricsProvider.getSystemMetrics(); - return systemMetrics.getTotalCpuUsedPercentage(); + return systemMetrics.getSystemCpuUsagePercentage(); }); MasterServerMetrics.registerMasterMemoryAvailableGauge(() -> { SystemMetrics systemMetrics = metricsProvider.getSystemMetrics(); diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/MasterServerLoadProtection.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/MasterServerLoadProtection.java index 03570d691d2b..6b259738fed2 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/MasterServerLoadProtection.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/MasterServerLoadProtection.java @@ -17,57 +17,11 @@ package org.apache.dolphinscheduler.server.master.config; -import org.apache.dolphinscheduler.meter.metrics.SystemMetrics; +import org.apache.dolphinscheduler.meter.metrics.BaseServerLoadProtection; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@Data -@NoArgsConstructor -@AllArgsConstructor -public class MasterServerLoadProtection { - - private boolean enabled = true; - - private double maxCpuUsagePercentageThresholds = 0.7; - - private double maxJVMMemoryUsagePercentageThresholds = 0.7; - - private double maxSystemMemoryUsagePercentageThresholds = 0.7; - - private double maxDiskUsagePercentageThresholds = 0.7; - - public boolean isOverload(SystemMetrics systemMetrics) { - if (!enabled) { - return false; - } - if (systemMetrics.getTotalCpuUsedPercentage() > maxCpuUsagePercentageThresholds) { - log.info( - "Master OverLoad: the TotalCpuUsedPercentage: {} is over then the MaxCpuUsagePercentageThresholds {}", - systemMetrics.getTotalCpuUsedPercentage(), maxCpuUsagePercentageThresholds); - return true; - } - if (systemMetrics.getJvmMemoryUsedPercentage() > maxJVMMemoryUsagePercentageThresholds) { - log.info( - "Master OverLoad: the JvmMemoryUsedPercentage: {} is over then the MaxJVMMemoryUsagePercentageThresholds {}", - systemMetrics.getJvmMemoryUsedPercentage(), maxCpuUsagePercentageThresholds); - return true; - } - if (systemMetrics.getDiskUsedPercentage() > maxDiskUsagePercentageThresholds) { - log.info("Master OverLoad: the DiskUsedPercentage: {} is over then the MaxDiskUsagePercentageThresholds {}", - systemMetrics.getDiskUsedPercentage(), maxCpuUsagePercentageThresholds); - return true; - } - if (systemMetrics.getSystemMemoryUsedPercentage() > maxSystemMemoryUsagePercentageThresholds) { - log.info( - "Master OverLoad: the SystemMemoryUsedPercentage: {} is over then the MaxSystemMemoryUsagePercentageThresholds {}", - systemMetrics.getSystemMemoryUsedPercentage(), maxSystemMemoryUsagePercentageThresholds); - return true; - } - return false; - } +public class MasterServerLoadProtection extends BaseServerLoadProtection { } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/metrics/MasterServerMetrics.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/metrics/MasterServerMetrics.java index 09ba1cb4ba3e..1fc92200df11 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/metrics/MasterServerMetrics.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/metrics/MasterServerMetrics.java @@ -51,7 +51,7 @@ public void registerMasterMemoryAvailableGauge(Supplier supplier) { public void registerMasterCpuUsageGauge(Supplier supplier) { Gauge.builder("ds.master.cpu.usage", supplier) - .description("worker cpu usage") + .description("master cpu usage") .register(Metrics.globalRegistry); } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/MasterSlotManager.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/MasterSlotManager.java index 834f56c2a456..155b97311796 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/MasterSlotManager.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/MasterSlotManager.java @@ -70,7 +70,8 @@ public class SlotChangeListener implements MasterInfoChangeListener { public void notify(Map masterNodeInfo) { List serverList = masterNodeInfo.values().stream() .filter(heartBeat -> !heartBeat.getServerStatus().equals(ServerStatus.BUSY)) - .map(this::convertHeartBeatToServer).collect(Collectors.toList()); + .map(this::convertHeartBeatToServer) + .collect(Collectors.toList()); syncMasterNodes(serverList); } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/ServerNodeManager.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/ServerNodeManager.java index 11a994aacdb1..258acd8f6e92 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/ServerNodeManager.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/ServerNodeManager.java @@ -249,7 +249,9 @@ private void updateWorkerNodes() { try { Map workerNodeMaps = registryClient.getServerMaps(RegistryNodeType.WORKER); for (Map.Entry entry : workerNodeMaps.entrySet()) { - workerNodeInfo.put(entry.getKey(), JSONUtils.parseObject(entry.getValue(), WorkerHeartBeat.class)); + String nodeAddress = entry.getKey(); + WorkerHeartBeat workerHeartBeat = JSONUtils.parseObject(entry.getValue(), WorkerHeartBeat.class); + workerNodeInfo.put(nodeAddress, workerHeartBeat); } } finally { workerGroupWriteLock.unlock(); diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/task/MasterHeartBeatTask.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/task/MasterHeartBeatTask.java index e9b0970ed300..b7b5e7a21e45 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/task/MasterHeartBeatTask.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/task/MasterHeartBeatTask.java @@ -64,7 +64,8 @@ public MasterHeartBeat getHeartBeat() { return MasterHeartBeat.builder() .startupTime(ServerLifeCycleManager.getServerStartupTime()) .reportTime(System.currentTimeMillis()) - .cpuUsage(systemMetrics.getTotalCpuUsedPercentage()) + .jvmCpuUsage(systemMetrics.getJvmCpuUsagePercentage()) + .cpuUsage(systemMetrics.getSystemCpuUsagePercentage()) .jvmMemoryUsage(systemMetrics.getJvmMemoryUsedPercentage()) .memoryUsage(systemMetrics.getSystemMemoryUsedPercentage()) .diskUsage(systemMetrics.getDiskUsedPercentage()) diff --git a/dolphinscheduler-master/src/main/resources/application.yaml b/dolphinscheduler-master/src/main/resources/application.yaml index ce7b1df7ddbc..a85eadb59f01 100644 --- a/dolphinscheduler-master/src/main/resources/application.yaml +++ b/dolphinscheduler-master/src/main/resources/application.yaml @@ -122,10 +122,10 @@ master: server-load-protection: # If set true, will open master overload protection enabled: true - # Master max cpu usage, when the master's cpu usage is smaller then this value, master server can execute workflow. - max-cpu-usage-percentage-thresholds: 0.7 - # Master max JVM memory usage , when the master's jvm memory usage is smaller then this value, master server can execute workflow. - max-jvm-memory-usage-percentage-thresholds: 0.7 + # Master max system cpu usage, when the master's system cpu usage is smaller then this value, master server can execute workflow. + max-system-cpu-usage-percentage-thresholds: 0.7 + # Master max jvm cpu usage, when the master's jvm cpu usage is smaller then this value, master server can execute workflow. + max-jvm-cpu-usage-percentage-thresholds: 0.7 # Master max System memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. max-system-memory-usage-percentage-thresholds: 0.7 # Master max disk usage , when the master's disk usage is smaller then this value, master server can execute workflow. diff --git a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterConfigTest.java b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterConfigTest.java index ed982933d287..faab44cf854c 100644 --- a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterConfigTest.java +++ b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterConfigTest.java @@ -17,16 +17,15 @@ package org.apache.dolphinscheduler.server.master.config; -import org.junit.jupiter.api.Assertions; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringExtension; -@ActiveProfiles("master") -@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc @SpringBootTest(classes = MasterConfig.class) public class MasterConfigTest { @@ -36,6 +35,18 @@ public class MasterConfigTest { @Test public void getMasterDispatchTaskNumber() { int masterDispatchTaskNumber = masterConfig.getDispatchTaskNumber(); - Assertions.assertEquals(3, masterDispatchTaskNumber); + assertEquals(30, masterDispatchTaskNumber); + } + + @Test + public void getServerLoadProtection() { + MasterServerLoadProtection serverLoadProtection = masterConfig.getServerLoadProtection(); + assertTrue(serverLoadProtection.isEnabled()); + assertEquals(0.77, serverLoadProtection.getMaxSystemCpuUsagePercentageThresholds()); + assertEquals(0.77, serverLoadProtection.getMaxJvmCpuUsagePercentageThresholds()); + assertEquals(0.77, serverLoadProtection.getMaxJvmCpuUsagePercentageThresholds()); + assertEquals(0.77, serverLoadProtection.getMaxSystemMemoryUsagePercentageThresholds()); + assertEquals(0.77, serverLoadProtection.getMaxDiskUsagePercentageThresholds()); + } } diff --git a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterServerLoadProtectionTest.java b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterServerLoadProtectionTest.java index 90627f99d35b..ce12eb1bd94f 100644 --- a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterServerLoadProtectionTest.java +++ b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterServerLoadProtectionTest.java @@ -30,7 +30,8 @@ void isOverload() { SystemMetrics systemMetrics = SystemMetrics.builder() .jvmMemoryUsedPercentage(0.71) .systemMemoryUsedPercentage(0.71) - .totalCpuUsedPercentage(0.71) + .systemCpuUsagePercentage(0.71) + .jvmCpuUsagePercentage(0.71) .diskUsedPercentage(0.71) .build(); masterServerLoadProtection.setEnabled(false); diff --git a/dolphinscheduler-master/src/test/resources/application.yaml b/dolphinscheduler-master/src/test/resources/application.yaml new file mode 100644 index 000000000000..0dbe490af394 --- /dev/null +++ b/dolphinscheduler-master/src/test/resources/application.yaml @@ -0,0 +1,164 @@ +# +# 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. +# +spring: + banner: + charset: UTF-8 + jackson: + time-zone: UTC + date-format: "yyyy-MM-dd HH:mm:ss" + cache: + # default enable cache, you can disable by `type: none` + type: none + cache-names: + - tenant + - user + - processDefinition + - processTaskRelation + - taskDefinition + caffeine: + spec: maximumSize=100,expireAfterWrite=300s,recordStats + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://127.0.0.1:5432/dolphinscheduler + username: root + password: root + hikari: + connection-test-query: select 1 + minimum-idle: 5 + auto-commit: true + validation-timeout: 3000 + pool-name: DolphinScheduler + maximum-pool-size: 50 + connection-timeout: 30000 + idle-timeout: 600000 + leak-detection-threshold: 0 + initialization-fail-timeout: 1 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: never + properties: + org.quartz.threadPool.threadPriority: 5 + org.quartz.jobStore.isClustered: true + org.quartz.jobStore.class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + org.quartz.scheduler.instanceId: AUTO + org.quartz.jobStore.tablePrefix: QRTZ_ + org.quartz.jobStore.acquireTriggersWithinLock: true + org.quartz.scheduler.instanceName: DolphinScheduler + org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool + org.quartz.jobStore.useProperties: false + org.quartz.threadPool.makeThreadsDaemons: true + org.quartz.threadPool.threadCount: 25 + org.quartz.jobStore.misfireThreshold: 60000 + org.quartz.scheduler.batchTriggerAcquisitionMaxCount: 1 + org.quartz.scheduler.makeSchedulerThreadDaemon: true + org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate + org.quartz.jobStore.clusterCheckinInterval: 5000 + +# Mybatis-plus configuration, you don't need to change it +mybatis-plus: + mapper-locations: classpath:org/apache/dolphinscheduler/dao/mapper/*Mapper.xml + type-aliases-package: org.apache.dolphinscheduler.dao.entity + configuration: + cache-enabled: false + call-setters-on-nulls: true + map-underscore-to-camel-case: true + jdbc-type-for-null: NULL + global-config: + db-config: + id-type: auto + banner: false + + +registry: + type: zookeeper + zookeeper: + namespace: dolphinscheduler + connect-string: localhost:2181 + retry-policy: + base-sleep-time: 60ms + max-sleep: 300ms + max-retries: 5 + session-timeout: 30s + connection-timeout: 9s + block-until-connected: 600ms + digest: ~ + +master: + listen-port: 5678 + # master fetch command num + fetch-command-num: 10 + # master prepare execute thread number to limit handle commands in parallel + pre-exec-threads: 10 + # master execute thread number to limit process instances in parallel + exec-threads: 100 + # master dispatch task number per batch, if all the tasks dispatch failed in a batch, will sleep 1s. + dispatch-task-number: 30 + # master host selector to select a suitable worker, default value: LowerWeight. Optional values include random, round_robin, lower_weight + host-selector: lower_weight + # master heartbeat interval + max-heartbeat-interval: 10s + # master commit task retry times + task-commit-retry-times: 5 + # master commit task interval + task-commit-interval: 1s + state-wheel-interval: 5s + server-load-protection: + # If set true, will open master overload protection + enabled: true + # Master max system cpu usage, when the master's system cpu usage is smaller then this value, master server can execute workflow. + max-system-cpu-usage-percentage-thresholds: 0.77 + # Master max jvm cpu usage, when the master's jvm cpu usage is smaller then this value, master server can execute workflow. + max-jvm-cpu-usage-percentage-thresholds: 0.77 + # Master max System memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. + max-system-memory-usage-percentage-thresholds: 0.77 + # Master max disk usage , when the master's disk usage is smaller then this value, master server can execute workflow. + max-disk-usage-percentage-thresholds: 0.77 + # failover interval, the unit is minute + failover-interval: 10m + # kill yarn / k8s application when failover taskInstance, default true + kill-application-when-task-failover: true + registry-disconnect-strategy: + # The disconnect strategy: stop, waiting + strategy: waiting + # The max waiting time to reconnect to registry if you set the strategy to waiting + max-waiting-time: 100s + worker-group-refresh-interval: 10s + +server: + port: 5679 + +management: + endpoints: + web: + exposure: + include: health,metrics,prometheus + endpoint: + health: + enabled: true + show-details: always + health: + db: + enabled: true + defaults: + enabled: false + metrics: + tags: + application: ${spring.application.name} + +metrics: + enabled: true diff --git a/dolphinscheduler-master/src/test/resources/logback.xml b/dolphinscheduler-master/src/test/resources/logback.xml index deb791fae21c..4470a4639109 100644 --- a/dolphinscheduler-master/src/test/resources/logback.xml +++ b/dolphinscheduler-master/src/test/resources/logback.xml @@ -66,12 +66,6 @@ - - - - - - - + diff --git a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/BaseServerLoadProtection.java b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/BaseServerLoadProtection.java new file mode 100644 index 000000000000..fd12d3bb6604 --- /dev/null +++ b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/BaseServerLoadProtection.java @@ -0,0 +1,67 @@ +/* + * 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.meter.metrics; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +public class BaseServerLoadProtection implements ServerLoadProtection { + + protected boolean enabled = true; + + protected double maxSystemCpuUsagePercentageThresholds = 0.7; + + protected double maxJvmCpuUsagePercentageThresholds = 0.7; + + protected double maxSystemMemoryUsagePercentageThresholds = 0.7; + + protected double maxDiskUsagePercentageThresholds = 0.7; + + @Override + public boolean isOverload(SystemMetrics systemMetrics) { + if (!enabled) { + return false; + } + if (systemMetrics.getSystemCpuUsagePercentage() > maxSystemCpuUsagePercentageThresholds) { + log.info( + "OverLoad: the system cpu usage: {} is over then the maxSystemCpuUsagePercentageThresholds {}", + systemMetrics.getSystemCpuUsagePercentage(), maxSystemCpuUsagePercentageThresholds); + return true; + } + if (systemMetrics.getJvmCpuUsagePercentage() > maxJvmCpuUsagePercentageThresholds) { + log.info( + "OverLoad: the jvm cpu usage: {} is over then the maxJvmCpuUsagePercentageThresholds {}", + systemMetrics.getJvmCpuUsagePercentage(), maxJvmCpuUsagePercentageThresholds); + return true; + } + if (systemMetrics.getDiskUsedPercentage() > maxDiskUsagePercentageThresholds) { + log.info("OverLoad: the DiskUsedPercentage: {} is over then the maxDiskUsagePercentageThresholds {}", + systemMetrics.getDiskUsedPercentage(), maxDiskUsagePercentageThresholds); + return true; + } + if (systemMetrics.getSystemMemoryUsedPercentage() > maxSystemMemoryUsagePercentageThresholds) { + log.info( + "OverLoad: the SystemMemoryUsedPercentage: {} is over then the maxSystemMemoryUsagePercentageThresholds {}", + systemMetrics.getSystemMemoryUsedPercentage(), maxSystemMemoryUsagePercentageThresholds); + return true; + } + return false; + } +} diff --git a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/DefaultMetricsProvider.java b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/DefaultMetricsProvider.java index 0ce6ceb4a401..f1240a054117 100644 --- a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/DefaultMetricsProvider.java +++ b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/DefaultMetricsProvider.java @@ -19,7 +19,6 @@ import org.apache.dolphinscheduler.common.utils.OSUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import io.micrometer.core.instrument.MeterRegistry; @@ -27,8 +26,11 @@ @Component public class DefaultMetricsProvider implements MetricsProvider { - @Autowired - private MeterRegistry meterRegistry; + private final MeterRegistry meterRegistry; + + public DefaultMetricsProvider(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } private SystemMetrics systemMetrics; @@ -53,8 +55,7 @@ public SystemMetrics getSystemMetrics() { systemMetrics = SystemMetrics.builder() .systemCpuUsagePercentage(systemCpuUsage) - .processCpuUsagePercentage(processCpuUsage) - .totalCpuUsedPercentage(systemCpuUsage + processCpuUsage) + .jvmCpuUsagePercentage(processCpuUsage) .jvmMemoryUsed(jvmMemoryUsed) .jvmMemoryMax(jvmMemoryMax) .jvmMemoryUsedPercentage(jvmMemoryUsed / jvmMemoryMax) diff --git a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/ServerLoadProtection.java b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/ServerLoadProtection.java new file mode 100644 index 000000000000..3385de891f3b --- /dev/null +++ b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/ServerLoadProtection.java @@ -0,0 +1,24 @@ +/* + * 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.meter.metrics; + +public interface ServerLoadProtection { + + boolean isOverload(SystemMetrics systemMetrics); + +} diff --git a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/SystemMetrics.java b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/SystemMetrics.java index dcffafb83dee..6da8f8ca4ece 100644 --- a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/SystemMetrics.java +++ b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/SystemMetrics.java @@ -30,8 +30,7 @@ public class SystemMetrics { // CPU private double systemCpuUsagePercentage; - private double processCpuUsagePercentage; - private double totalCpuUsedPercentage; + private double jvmCpuUsagePercentage; // JVM-Memory // todo: get pod memory usage diff --git a/dolphinscheduler-standalone-server/src/main/resources/application.yaml b/dolphinscheduler-standalone-server/src/main/resources/application.yaml index 8e58b8956804..d26ea5a4e0aa 100644 --- a/dolphinscheduler-standalone-server/src/main/resources/application.yaml +++ b/dolphinscheduler-standalone-server/src/main/resources/application.yaml @@ -190,10 +190,10 @@ master: state-wheel-interval: 5s server-load-protection: enabled: true - # Master max cpu usage, when the master's cpu usage is smaller then this value, master server can execute workflow. - max-cpu-usage-percentage-thresholds: 0.9 - # Master max JVM memory usage , when the master's jvm memory usage is smaller then this value, master server can execute workflow. - max-jvm-memory-usage-percentage-thresholds: 0.9 + # Master max system cpu usage, when the master's system cpu usage is smaller then this value, master server can execute workflow. + max-system-cpu-usage-percentage-thresholds: 0.9 + # Master max jvm cpu usage, when the master's jvm cpu usage is smaller then this value, master server can execute workflow. + max-jvm-cpu-usage-percentage-thresholds: 0.9 # Master max System memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. max-system-memory-usage-percentage-thresholds: 0.9 # Master max disk usage , when the master's disk usage is smaller then this value, master server can execute workflow. @@ -215,10 +215,10 @@ worker: host-weight: 100 server-load-protection: enabled: true - # Worker max cpu usage, when the worker's cpu usage is smaller then this value, worker server can be dispatched tasks. - max-cpu-usage-percentage-thresholds: 0.9 - # Worker max JVM memory usage , when the worker's jvm memory usage is smaller then this value, worker server can be dispatched tasks. - max-jvm-memory-usage-percentage-thresholds: 0.9 + # Worker max system cpu usage, when the worker's system cpu usage is smaller then this value, worker server can be dispatched tasks. + max-system-cpu-usage-percentage-thresholds: 0.9 + # Worker max jvm cpu usage, when the worker's jvm cpu usage is smaller then this value, worker server can be dispatched tasks. + max-jvm-cpu-usage-percentage-thresholds: 0.9 # Worker max System memory usage , when the worker's system memory usage is smaller then this value, worker server can be dispatched tasks. max-system-memory-usage-percentage-thresholds: 0.9 # Worker max disk usage , when the worker's disk usage is smaller then this value, worker server can be dispatched tasks. diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java index 2420ae52535b..86e755fc8a24 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java @@ -94,7 +94,7 @@ public void run() { WorkerServerMetrics.registerWorkerCpuUsageGauge(() -> { SystemMetrics systemMetrics = metricsProvider.getSystemMetrics(); - return systemMetrics.getTotalCpuUsedPercentage(); + return systemMetrics.getSystemCpuUsagePercentage(); }); WorkerServerMetrics.registerWorkerMemoryAvailableGauge(() -> { SystemMetrics systemMetrics = metricsProvider.getSystemMetrics(); diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/config/WorkerServerLoadProtection.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/config/WorkerServerLoadProtection.java index 6e68a71bf524..1a52100eb26c 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/config/WorkerServerLoadProtection.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/config/WorkerServerLoadProtection.java @@ -17,57 +17,11 @@ package org.apache.dolphinscheduler.server.worker.config; -import org.apache.dolphinscheduler.meter.metrics.SystemMetrics; +import org.apache.dolphinscheduler.meter.metrics.BaseServerLoadProtection; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Data @Slf4j -@NoArgsConstructor -@AllArgsConstructor -public class WorkerServerLoadProtection { - - private boolean enabled = true; - - private double maxCpuUsagePercentageThresholds = 0.7; - - private double maxJVMMemoryUsagePercentageThresholds = 0.7; - - private double maxSystemMemoryUsagePercentageThresholds = 0.7; - - private double maxDiskUsagePercentageThresholds = 0.7; - - public boolean isOverload(SystemMetrics systemMetrics) { - if (!enabled) { - return false; - } - if (systemMetrics.getTotalCpuUsedPercentage() > maxCpuUsagePercentageThresholds) { - log.info( - "Worker OverLoad: the TotalCpuUsedPercentage: {} is over then the MaxCpuUsagePercentageThresholds {}", - systemMetrics.getTotalCpuUsedPercentage(), maxCpuUsagePercentageThresholds); - return true; - } - if (systemMetrics.getJvmMemoryUsedPercentage() > maxJVMMemoryUsagePercentageThresholds) { - log.info( - "Worker OverLoad: the JvmMemoryUsedPercentage: {} is over then the maxCpuUsagePercentageThresholds {}", - systemMetrics.getJvmMemoryUsedPercentage(), maxJVMMemoryUsagePercentageThresholds); - return true; - } - if (systemMetrics.getDiskUsedPercentage() > maxDiskUsagePercentageThresholds) { - log.info("Worker OverLoad: the DiskUsedPercentage: {} is over then the MaxCpuUsagePercentageThresholds {}", - systemMetrics.getDiskUsedPercentage(), maxDiskUsagePercentageThresholds); - return true; - } - if (systemMetrics.getSystemMemoryUsedPercentage() > maxSystemMemoryUsagePercentageThresholds) { - log.info( - "Worker OverLoad: the SystemMemoryUsedPercentage: {} is over then the MaxSystemMemoryUsagePercentageThresholds {}", - systemMetrics.getSystemMemoryUsedPercentage(), maxSystemMemoryUsagePercentageThresholds); - return true; - } - return false; - } +public class WorkerServerLoadProtection extends BaseServerLoadProtection { } diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/task/WorkerHeartBeatTask.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/task/WorkerHeartBeatTask.java index 57349e14489b..4eefd9df1084 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/task/WorkerHeartBeatTask.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/task/WorkerHeartBeatTask.java @@ -65,7 +65,8 @@ public WorkerHeartBeat getHeartBeat() { return WorkerHeartBeat.builder() .startupTime(ServerLifeCycleManager.getServerStartupTime()) .reportTime(System.currentTimeMillis()) - .cpuUsage(systemMetrics.getTotalCpuUsedPercentage()) + .jvmCpuUsage(systemMetrics.getJvmCpuUsagePercentage()) + .cpuUsage(systemMetrics.getSystemCpuUsagePercentage()) .jvmMemoryUsage(systemMetrics.getJvmMemoryUsedPercentage()) .memoryUsage(systemMetrics.getSystemMemoryUsedPercentage()) .diskUsage(systemMetrics.getDiskUsedPercentage()) diff --git a/dolphinscheduler-worker/src/main/resources/application.yaml b/dolphinscheduler-worker/src/main/resources/application.yaml index ad0535ac7630..4361e8f014e2 100644 --- a/dolphinscheduler-worker/src/main/resources/application.yaml +++ b/dolphinscheduler-worker/src/main/resources/application.yaml @@ -50,10 +50,10 @@ worker: server-load-protection: # If set true, will open worker overload protection enabled: true - # Worker max cpu usage, when the worker's cpu usage is smaller then this value, worker server can be dispatched tasks. - max-cpu-usage-percentage-thresholds: 0.7 - # Worker max jvm memory usage , when the worker's jvm memory usage is smaller then this value, worker server can be dispatched tasks. - max-jvm-memory-usage-percentage-thresholds: 0.7 + # Worker max system cpu usage, when the worker's system cpu usage is smaller then this value, worker server can be dispatched tasks. + max-system-cpu-usage-percentage-thresholds: 0.7 + # Worker max jvm cpu usage, when the worker's jvm cpu usage is smaller then this value, worker server can be dispatched tasks. + max-jvm-cpu-usage-percentage-thresholds: 0.7 # Worker max System memory usage , when the master's system memory usage is smaller then this value, master server can execute workflow. max-system-memory-usage-percentage-thresholds: 0.7 # Worker max disk usage , when the worker's disk usage is smaller then this value, worker server can be dispatched tasks. diff --git a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/config/WorkerServerLoadProtectionTest.java b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/config/WorkerServerLoadProtectionTest.java index 696e9c247839..204deb120eb9 100644 --- a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/config/WorkerServerLoadProtectionTest.java +++ b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/config/WorkerServerLoadProtectionTest.java @@ -30,7 +30,8 @@ void isOverload() { SystemMetrics systemMetrics = SystemMetrics.builder() .jvmMemoryUsedPercentage(0.71) .systemMemoryUsedPercentage(0.71) - .totalCpuUsedPercentage(0.71) + .systemCpuUsagePercentage(0.71) + .jvmCpuUsagePercentage(0.71) .diskUsedPercentage(0.71) .build(); workerServerLoadProtection.setEnabled(false); From 5050a8e1e680b4f0debf6ce59a22bd50afb7cb7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=BA=E9=98=B3?= Date: Tue, 9 Apr 2024 14:38:47 +0800 Subject: [PATCH 017/165] [Improve] Fix typo on ProcessServiceImpl (#15817) --- .../dolphinscheduler/service/process/ProcessServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java index 261637529ad7..2b3dbbebbb98 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java @@ -798,7 +798,7 @@ private Boolean checkCmdParam(Command command, Map cmdParam) { // recover tolerance fault process // If the workflow instance is in ready state, we will change to running, this can avoid the workflow // instance - // status is not correct with taskInsatnce status + // status is not correct with taskInstance status if (processInstance.getState() == WorkflowExecutionStatus.READY_PAUSE || processInstance.getState() == WorkflowExecutionStatus.READY_STOP) { // todo: If we handle the ready state in WorkflowExecuteRunnable then we can remove below code From b70bcf6570b4b11b5c58f485c85ee4987326c357 Mon Sep 17 00:00:00 2001 From: ikiler Date: Wed, 10 Apr 2024 09:28:37 +0800 Subject: [PATCH 018/165] fix dinky unresolve params --- .../plugin/task/dinky/DinkyTask.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-dinky/src/main/java/org/apache/dolphinscheduler/plugin/task/dinky/DinkyTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-dinky/src/main/java/org/apache/dolphinscheduler/plugin/task/dinky/DinkyTask.java index 2c0b0cb68e11..aa0c1feed1f6 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-dinky/src/main/java/org/apache/dolphinscheduler/plugin/task/dinky/DinkyTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-dinky/src/main/java/org/apache/dolphinscheduler/plugin/task/dinky/DinkyTask.java @@ -27,6 +27,7 @@ import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; import org.apache.dolphinscheduler.plugin.task.api.model.Property; import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; +import org.apache.dolphinscheduler.plugin.task.api.parser.PlaceholderUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; @@ -343,15 +344,30 @@ private Map generateVariables() { } } List localParams = this.dinkyParameters.getLocalParams(); + Map prepareParamsMap = taskExecutionContext.getPrepareParamsMap(); if (localParams == null || localParams.isEmpty()) { return variables; } + Map convertMap = convert(prepareParamsMap); for (Property property : localParams) { - variables.put(property.getProp(), property.getValue()); + String propertyValue = property.getValue(); + String value = PlaceholderUtils.replacePlaceholders(propertyValue, convertMap, true); + variables.put(property.getProp(), value); } return variables; } + public static Map convert(Map paramsMap) { + if (paramsMap == null) { + return null; + } + Map map = new HashMap<>(); + for (Map.Entry en : paramsMap.entrySet()) { + map.put(en.getKey(), en.getValue().getValue()); + } + return map; + } + private String getDinkyVersion(String address) { JsonNode versionJsonNode = parse(doGet(address + DinkyTaskConstants.GET_VERSION, new HashMap<>())); if (versionJsonNode instanceof MissingNode || versionJsonNode == null From f93b27e1217cab05e93953b71e5d2ea5e40fb8c8 Mon Sep 17 00:00:00 2001 From: sleo <97011595+alei1206@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:13:52 +0800 Subject: [PATCH 019/165] [Worker] Fix will not kill the subprocess in remote when stop a remote-shell task #15570 (#15629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix cannot kill the subprocess when stop a remote-shell task * move parse pid logic into ProcessUtils * extract common logic --------- Co-authored-by: 旺阳 Co-authored-by: Rick Cheng --- .../plugin/task/api/utils/ProcessUtils.java | 43 +++++++++++------- .../task/remoteshell/RemoteExecutor.java | 44 ++++++++++++++++++- .../task/remoteshell/RemoteExecutorTest.java | 19 ++++++++ 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/utils/ProcessUtils.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/utils/ProcessUtils.java index 7b61a1eaec40..e8e31faa6d31 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/utils/ProcessUtils.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/utils/ProcessUtils.java @@ -39,6 +39,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -117,33 +118,45 @@ public static boolean kill(@NonNull TaskExecutionContext request) { * @throws Exception exception */ public static String getPidsStr(int processId) throws Exception { - StringBuilder sb = new StringBuilder(); - Matcher mat = null; + + String rawPidStr; + // pstree pid get sub pids if (SystemUtils.IS_OS_MAC) { - String pids = OSUtils.exeCmd(String.format("%s -sp %d", TaskConstants.PSTREE, processId)); - if (StringUtils.isNotEmpty(pids)) { - mat = MACPATTERN.matcher(pids); + rawPidStr = OSUtils.exeCmd(String.format("%s -sp %d", TaskConstants.PSTREE, processId)); + } else if (SystemUtils.IS_OS_LINUX) { + rawPidStr = OSUtils.exeCmd(String.format("%s -p %d", TaskConstants.PSTREE, processId)); + } else { + rawPidStr = OSUtils.exeCmd(String.format("%s -p %d", TaskConstants.PSTREE, processId)); + } + + return parsePidStr(rawPidStr); + } + + public static String parsePidStr(String rawPidStr) { + + log.info("prepare to parse pid, raw pid string: {}", rawPidStr); + ArrayList allPidList = new ArrayList<>(); + Matcher mat = null; + if (SystemUtils.IS_OS_MAC) { + if (StringUtils.isNotEmpty(rawPidStr)) { + mat = MACPATTERN.matcher(rawPidStr); } } else if (SystemUtils.IS_OS_LINUX) { - String pids = OSUtils.exeCmd(String.format("%s -p %d", TaskConstants.PSTREE, processId)); - if (StringUtils.isNotEmpty(pids)) { - mat = LINUXPATTERN.matcher(pids); + if (StringUtils.isNotEmpty(rawPidStr)) { + mat = LINUXPATTERN.matcher(rawPidStr); } } else { - String pids = OSUtils.exeCmd(String.format("%s -p %d", TaskConstants.PSTREE, processId)); - if (StringUtils.isNotEmpty(pids)) { - mat = WINDOWSPATTERN.matcher(pids); + if (StringUtils.isNotEmpty(rawPidStr)) { + mat = WINDOWSPATTERN.matcher(rawPidStr); } } - if (null != mat) { while (mat.find()) { - sb.append(mat.group(1)).append(" "); + allPidList.add(mat.group(1)); } } - - return sb.toString().trim(); + return String.join(" ", allPidList).trim(); } /** diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java index c590fa9e4452..334ebb61958f 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java @@ -17,12 +17,16 @@ package org.apache.dolphinscheduler.plugin.task.remoteshell; +import static org.apache.dolphinscheduler.plugin.task.remoteshell.RemoteExecutor.COMMAND.PSTREE_COMMAND; + import org.apache.dolphinscheduler.plugin.datasource.ssh.SSHUtils; import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHConnectionParam; import org.apache.dolphinscheduler.plugin.task.api.TaskException; import org.apache.dolphinscheduler.plugin.task.api.parser.TaskOutputParameterParser; +import org.apache.dolphinscheduler.plugin.task.api.utils.ProcessUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.channel.ChannelExec; import org.apache.sshd.client.channel.ClientChannelEvent; @@ -50,7 +54,6 @@ public class RemoteExecutor implements AutoCloseable { static final int TRACK_INTERVAL = 5000; protected Map taskOutputParams = new HashMap<>(); - private SshClient sshClient; private ClientSession session; private SSHConnectionParam sshConnectionParam; @@ -154,11 +157,45 @@ public void cleanData(String taskId) { public void kill(String taskId) throws IOException { String pid = getTaskPid(taskId); - String killCommand = String.format(COMMAND.KILL_COMMAND, pid); + + if (StringUtils.isEmpty(pid)) { + log.warn("query remote-shell task remote process id with empty"); + return; + } + if (!NumberUtils.isParsable(pid)) { + log.error("query remote-shell task remote process id error, pid {} can not parse to number", pid); + return; + } + + // query all pid + String remotePidStr = getAllRemotePidStr(pid); + String killCommand = String.format(COMMAND.KILL_COMMAND, remotePidStr); + log.info("prepare to execute kill command in host: {}, kill cmd: {}", sshConnectionParam.getHost(), + killCommand); runRemote(killCommand); cleanData(taskId); } + protected String getAllRemotePidStr(String pid) { + + String remoteProcessIdStr = ""; + String cmd = String.format(PSTREE_COMMAND, pid); + log.info("query all process id cmd: {}", cmd); + + try { + String rawPidStr = runRemote(cmd); + remoteProcessIdStr = ProcessUtils.parsePidStr(rawPidStr); + if (!remoteProcessIdStr.startsWith(pid)) { + log.error("query remote process id error, [{}] first pid not equal [{}]", remoteProcessIdStr, pid); + remoteProcessIdStr = pid; + } + } catch (Exception e) { + log.error("query remote all process id error", e); + remoteProcessIdStr = pid; + } + return remoteProcessIdStr; + } + public String getTaskPid(String taskId) throws IOException { String pidCommand = String.format(COMMAND.GET_PID_COMMAND, taskId); return runRemote(pidCommand).trim(); @@ -238,6 +275,9 @@ private COMMAND() { static final String ADD_STATUS_COMMAND = "\necho %s$?"; static final String CAT_FINAL_SCRIPT = "cat %s%s.sh"; + + static final String PSTREE_COMMAND = "pstree -p %s"; + } } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java index 975f05969582..3cc9757ce161 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.plugin.task.remoteshell; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -135,4 +136,22 @@ void testGetTaskExitCode() throws IOException { doReturn("DOLPHINSCHEDULER-REMOTE-SHELL-TASK-STATUS-1").when(remoteExecutor).runRemote(trackCommand); Assertions.assertEquals(1, remoteExecutor.getTaskExitCode(taskId)); } + + @Test + void getAllRemotePidStr() throws IOException { + + RemoteExecutor remoteExecutor = spy(new RemoteExecutor(sshConnectionParam)); + doReturn("bash(9527)───sleep(9528)").when(remoteExecutor).runRemote(anyString()); + String allPidStr = remoteExecutor.getAllRemotePidStr("9527"); + Assertions.assertEquals("9527 9528", allPidStr); + + doReturn("systemd(1)───sleep(9528)").when(remoteExecutor).runRemote(anyString()); + allPidStr = remoteExecutor.getAllRemotePidStr("9527"); + Assertions.assertEquals("9527", allPidStr); + + doThrow(new TaskException()).when(remoteExecutor).runRemote(anyString()); + allPidStr = remoteExecutor.getAllRemotePidStr("9527"); + Assertions.assertEquals("9527", allPidStr); + + } } From 2b5f8433b732eee8c31a05f741636a007d5f2344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=A7=E5=9F=8E?= Date: Thu, 11 Apr 2024 14:13:53 +0800 Subject: [PATCH 020/165] Fix cannot construct instance of StreamingTaskTriggerResponse --- .../master/transportor/StreamingTaskTriggerResponse.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-master/src/main/java/org/apache/dolphinscheduler/extract/master/transportor/StreamingTaskTriggerResponse.java b/dolphinscheduler-extract/dolphinscheduler-extract-master/src/main/java/org/apache/dolphinscheduler/extract/master/transportor/StreamingTaskTriggerResponse.java index 0f9f2652802b..d25611aee004 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-master/src/main/java/org/apache/dolphinscheduler/extract/master/transportor/StreamingTaskTriggerResponse.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-master/src/main/java/org/apache/dolphinscheduler/extract/master/transportor/StreamingTaskTriggerResponse.java @@ -19,9 +19,11 @@ import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data @AllArgsConstructor +@NoArgsConstructor public class StreamingTaskTriggerResponse { private boolean success; From 883848f6ee00843d3c9e4668d607336d544dd110 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 11 Apr 2024 19:28:00 +0800 Subject: [PATCH 021/165] Remove unused caffeine cache (#15830) --- dolphinscheduler-master/pom.xml | 4 ---- .../src/main/resources/application.yaml | 11 ----------- .../src/test/resources/application.yaml | 11 ----------- .../src/main/resources/application.yaml | 11 ----------- 4 files changed, 37 deletions(-) diff --git a/dolphinscheduler-master/pom.xml b/dolphinscheduler-master/pom.xml index 31627d4b7fce..1b99dd97f3e9 100644 --- a/dolphinscheduler-master/pom.xml +++ b/dolphinscheduler-master/pom.xml @@ -102,10 +102,6 @@ org.codehaus.janino janino - - com.github.ben-manes.caffeine - caffeine - org.apache.hbase.thirdparty diff --git a/dolphinscheduler-master/src/main/resources/application.yaml b/dolphinscheduler-master/src/main/resources/application.yaml index a85eadb59f01..c2d8f5e787cf 100644 --- a/dolphinscheduler-master/src/main/resources/application.yaml +++ b/dolphinscheduler-master/src/main/resources/application.yaml @@ -22,17 +22,6 @@ spring: jackson: time-zone: UTC date-format: "yyyy-MM-dd HH:mm:ss" - cache: - # default enable cache, you can disable by `type: none` - type: none - cache-names: - - tenant - - user - - processDefinition - - processTaskRelation - - taskDefinition - caffeine: - spec: maximumSize=100,expireAfterWrite=300s,recordStats datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://127.0.0.1:5432/dolphinscheduler diff --git a/dolphinscheduler-master/src/test/resources/application.yaml b/dolphinscheduler-master/src/test/resources/application.yaml index 0dbe490af394..f4827d4b3c56 100644 --- a/dolphinscheduler-master/src/test/resources/application.yaml +++ b/dolphinscheduler-master/src/test/resources/application.yaml @@ -20,17 +20,6 @@ spring: jackson: time-zone: UTC date-format: "yyyy-MM-dd HH:mm:ss" - cache: - # default enable cache, you can disable by `type: none` - type: none - cache-names: - - tenant - - user - - processDefinition - - processTaskRelation - - taskDefinition - caffeine: - spec: maximumSize=100,expireAfterWrite=300s,recordStats datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://127.0.0.1:5432/dolphinscheduler diff --git a/dolphinscheduler-standalone-server/src/main/resources/application.yaml b/dolphinscheduler-standalone-server/src/main/resources/application.yaml index d26ea5a4e0aa..654113471de5 100644 --- a/dolphinscheduler-standalone-server/src/main/resources/application.yaml +++ b/dolphinscheduler-standalone-server/src/main/resources/application.yaml @@ -23,17 +23,6 @@ spring: date-format: "yyyy-MM-dd HH:mm:ss" banner: charset: UTF-8 - cache: - # default enable cache, you can disable by `type: none` - type: none - cache-names: - - tenant - - user - - processDefinition - - processTaskRelation - - taskDefinition - caffeine: - spec: maximumSize=100,expireAfterWrite=300s,recordStats sql: init: schema-locations: classpath:sql/dolphinscheduler_h2.sql From e5e77492518a198877171801c1cf484b86f852e3 Mon Sep 17 00:00:00 2001 From: BaiJv Date: Fri, 12 Apr 2024 10:06:32 +0800 Subject: [PATCH 022/165] [Improvement] Abnormal characters check (#15824) * abnormal characters check * add test case * remove error log * fix code style * fix import --- .../service/impl/ResourcesServiceImpl.java | 5 +++++ .../api/utils/CheckUtils.java | 10 ++++++++++ .../api/utils/CheckUtilsTest.java | 20 +++++++++++++++++++ .../common/constants/Constants.java | 5 +++++ 4 files changed, 40 insertions(+) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ResourcesServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ResourcesServiceImpl.java index 6a15da17a804..1c039cdfbdff 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ResourcesServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ResourcesServiceImpl.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.api.service.impl; +import static org.apache.dolphinscheduler.api.utils.CheckUtils.checkFilePath; import static org.apache.dolphinscheduler.common.constants.Constants.ALIAS; import static org.apache.dolphinscheduler.common.constants.Constants.CONTENT; import static org.apache.dolphinscheduler.common.constants.Constants.EMPTY_STRING; @@ -1290,6 +1291,10 @@ private void checkFullName(String userTenantCode, String fullName) { if (FOLDER_SEPARATOR.equalsIgnoreCase(fullName)) { return; } + // abnormal characters check + if (!checkFilePath(fullName)) { + throw new ServiceException(Status.ILLEGAL_RESOURCE_PATH); + } // Avoid returning to the parent directory if (fullName.contains("../")) { throw new ServiceException(Status.ILLEGAL_RESOURCE_PATH, fullName); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/CheckUtils.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/CheckUtils.java index 8b166a16ddb6..b394d4956c93 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/CheckUtils.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/utils/CheckUtils.java @@ -158,4 +158,14 @@ private static boolean regexChecks(String str, Pattern pattern) { return pattern.matcher(str).matches(); } + + /** + * regex FilePath check,only use a to z, A to Z, 0 to 9, and _./- + * + * @param str input string + * @return true if regex pattern is right, otherwise return false + */ + public static boolean checkFilePath(String str) { + return regexChecks(str, Constants.REGEX_FILE_PATH); + } } diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/CheckUtilsTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/CheckUtilsTest.java index bca8a69a16f3..da5ea88c835b 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/CheckUtilsTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/CheckUtilsTest.java @@ -92,4 +92,24 @@ public void testCheckPhone() { Assertions.assertTrue(CheckUtils.checkPhone("17362537263")); } + /** + * check file path + */ + @Test + public void testCheckFilePath() { + // true + Assertions.assertTrue(CheckUtils.checkFilePath("/")); + Assertions.assertTrue(CheckUtils.checkFilePath("xx/")); + Assertions.assertTrue(CheckUtils.checkFilePath("/xx")); + Assertions.assertTrue(CheckUtils.checkFilePath("14567134578654")); + Assertions.assertTrue(CheckUtils.checkFilePath("/admin/root/")); + Assertions.assertTrue(CheckUtils.checkFilePath("/admin/root/1531531..13513/153135..")); + // false + Assertions.assertFalse(CheckUtils.checkFilePath(null)); + Assertions.assertFalse(CheckUtils.checkFilePath("file://xxx/ss")); + Assertions.assertFalse(CheckUtils.checkFilePath("/xxx/ss;/dasd/123")); + Assertions.assertFalse(CheckUtils.checkFilePath("/xxx/ss && /dasd/123")); + Assertions.assertFalse(CheckUtils.checkFilePath("/xxx/ss || /dasd/123")); + } + } diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java index 054a9410d5f1..19e1a1fabbc7 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java @@ -252,6 +252,11 @@ private Constants() { */ public static final Pattern REGEX_USER_NAME = Pattern.compile("^[a-zA-Z0-9._-]{3,39}$"); + /** + * file path regex + */ + public static final Pattern REGEX_FILE_PATH = Pattern.compile("^[a-zA-Z0-9_./-]+$"); + /** * read permission */ From 08ac1322864edf42903c7c03942fcad62c37da35 Mon Sep 17 00:00:00 2001 From: BaiJv Date: Fri, 12 Apr 2024 14:28:13 +0800 Subject: [PATCH 023/165] [Improvement] Modify python-gateway: enabled default to false. (#15825) * remove python-gateway:auth-token,modify python gateway: enabled default to false. * reset token --- dolphinscheduler-api/src/main/resources/application.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dolphinscheduler-api/src/main/resources/application.yaml b/dolphinscheduler-api/src/main/resources/application.yaml index c93fd99a590e..61f9c8a592d1 100644 --- a/dolphinscheduler-api/src/main/resources/application.yaml +++ b/dolphinscheduler-api/src/main/resources/application.yaml @@ -149,8 +149,8 @@ api: #tenant1: 11 #tenant2: 20 python-gateway: - # Weather enable python gateway server or not. The default value is true. - enabled: true + # Weather enable python gateway server or not. The default value is false. + enabled: false # Authentication token for connection from python api to python gateway server. Should be changed the default value # when you deploy in public network. auth-token: jwUDzpLsNKEFER4*a8gruBH_GsAurNxU7A@Xc From efbffacfbecd3632388e69ed400e3433ecc35f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=BA=E9=98=B3?= Date: Mon, 15 Apr 2024 11:29:11 +0800 Subject: [PATCH 024/165] delete useless code (#15844) --- .../dolphinscheduler/dao/mapper/UdfFuncMapper.java | 7 ------- .../dolphinscheduler/dao/mapper/UdfFuncMapper.xml | 12 ------------ .../dao/mapper/UdfFuncMapperTest.java | 13 ------------- 3 files changed, 32 deletions(-) diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/UdfFuncMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/UdfFuncMapper.java index 6bc8049c7dd3..abc12f9aa19d 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/UdfFuncMapper.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/UdfFuncMapper.java @@ -109,13 +109,6 @@ List getUdfFuncByType(@Param("ids") List ids, */ List listAuthorizedUdfByResourceId(@Param("userId") int userId, @Param("resourceIds") int[] resourceIds); - /** - * batch update udf func - * @param udfFuncList udf list - * @return update num - */ - int batchUpdateUdfFunc(@Param("udfFuncList") List udfFuncList); - /** * listAuthorizedUdfByUserId * @param userId diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/UdfFuncMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/UdfFuncMapper.xml index 24cb8de95626..84f9fd970837 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/UdfFuncMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/UdfFuncMapper.xml @@ -171,18 +171,6 @@ - - - update t_ds_udfs - - resource_name=#{udf.resourceName}, - update_time=#{udf.updateTime} - - - id=#{udf.id} - - - @@ -38,30 +38,25 @@ and log.time > #{startDate} and log.time #{endDate} - - and log.resource_type in - - #{i} + + and log.model_type in + + #{model_type} - - and log.operation in - - #{j} + + and log.operation_type in + + #{operation_type} - and u.user_name = #{userName} + and u.user_name like concat ('%', #{userName}, '%') + + + and log.model_name like concat ('%', #{modelName}, '%') order by log.time desc - diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql index 3c7b8805a005..4187b93a1c87 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql @@ -2020,10 +2020,14 @@ CREATE TABLE t_ds_audit_log ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, - resource_type int(11) NOT NULL, - operation int(11) NOT NULL, + model_id bigint(20) NOT NULL, + model_name varchar(255) NOT NULL, + model_type varchar(255) NOT NULL, + operation_type varchar(255) NOT NULL, + description varchar(255) NOT NULL, + latency int(11) NOT NULL, + detail varchar(255) DEFAULT NULL, time timestamp NULL DEFAULT CURRENT_TIMESTAMP, - resource_id int(11) NOT NULL, PRIMARY KEY (id) ); diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql index 57401291eda5..f3d01c14b5db 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql @@ -2009,10 +2009,14 @@ DROP TABLE IF EXISTS `t_ds_audit_log`; CREATE TABLE `t_ds_audit_log` ( `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT'key', `user_id` int(11) NOT NULL COMMENT 'user id', - `resource_type` int(11) NOT NULL COMMENT 'resource type', - `operation` int(11) NOT NULL COMMENT 'operation', - `time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', - `resource_id` int(11) NULL DEFAULT NULL COMMENT 'resource id', + `model_id` bigint(20) DEFAULT NULL COMMENT 'model id', + `model_name` varchar(100) DEFAULT NULL COMMENT 'model name', + `model_type` varchar(100) NOT NULL COMMENT 'model type', + `operation_type` varchar(100) NOT NULL COMMENT 'operation type', + `description` varchar(100) DEFAULT NULL COMMENT 'api description', + `latency` int(11) DEFAULT NULL COMMENT 'api cost milliseconds', + `detail` varchar(100) DEFAULT NULL COMMENT 'object change detail', + `time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'operation time', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET=utf8 COLLATE = utf8_bin; diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql index 8313542ab8b3..5dd804c0c791 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql @@ -1993,11 +1993,15 @@ CREATE TABLE t_ds_task_group ( DROP TABLE IF EXISTS t_ds_audit_log; CREATE TABLE t_ds_audit_log ( id serial NOT NULL, - user_id int NOT NULL, - resource_type int NOT NULL, - operation int NOT NULL, - time timestamp DEFAULT NULL , - resource_id int NOT NULL, + user_id int NOT NULL, + model_id bigint NOT NULL, + model_name VARCHAR(255) NOT NULL, + model_type VARCHAR(255) NOT NULL, + operation_type VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL, + latency int NOT NULL, + detail VARCHAR(255) DEFAULT NULL, + time timestamp DEFAULT NULL , PRIMARY KEY (id) ); diff --git a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/mysql/dolphinscheduler_ddl.sql b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/mysql/dolphinscheduler_ddl.sql index 5ee7453d4676..b5e5b31d113d 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/mysql/dolphinscheduler_ddl.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/mysql/dolphinscheduler_ddl.sql @@ -25,4 +25,32 @@ CREATE TABLE `t_ds_relation_project_worker_group` ( UNIQUE KEY unique_project_worker_group(project_code,worker_group) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE = utf8_bin; -ALTER TABLE t_ds_project_parameter ADD `operator` int(11) DEFAULT NULL COMMENT 'operator user id'; \ No newline at end of file +ALTER TABLE t_ds_project_parameter ADD `operator` int(11) DEFAULT NULL COMMENT 'operator user id'; + +-- modify_data_t_ds_audit_log_input_entry behavior change +--DROP PROCEDURE if EXISTS modify_data_t_ds_audit_log_input_entry; +DROP PROCEDURE if EXISTS modify_data_t_ds_audit_log_input_entry; +delimiter d// +CREATE PROCEDURE modify_data_t_ds_audit_log_input_entry() +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_NAME='t_ds_audit_log' + AND TABLE_SCHEMA=(SELECT DATABASE()) + AND COLUMN_NAME ='resource_type') + THEN +ALTER TABLE `t_ds_audit_log` +drop resource_type, drop operation, drop resource_id, + add `model_id` bigint(20) DEFAULT NULL COMMENT 'model id', + add `model_name` varchar(100) DEFAULT NULL COMMENT 'model name', + add `model_type` varchar(100) NOT NULL COMMENT 'model type', + add `operation_type` varchar(100) NOT NULL COMMENT 'operation type', + add `description` varchar(100) DEFAULT NULL COMMENT 'api description', + add `latency` int(11) DEFAULT NULL COMMENT 'api cost milliseconds', + add `detail` varchar(100) DEFAULT NULL COMMENT 'object change detail', + MODIFY COLUMN `time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT "operation time"; +END IF; +END; +d// +delimiter ; +CALL modify_data_t_ds_audit_log_input_entry; +DROP PROCEDURE modify_data_t_ds_audit_log_input_entry; \ No newline at end of file diff --git a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/postgresql/dolphinscheduler_ddl.sql b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/postgresql/dolphinscheduler_ddl.sql index 90646557c078..22e1c599de96 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/postgresql/dolphinscheduler_ddl.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/postgresql/dolphinscheduler_ddl.sql @@ -29,4 +29,30 @@ DROP SEQUENCE IF EXISTS t_ds_relation_project_worker_group_sequence; CREATE SEQUENCE t_ds_relation_project_worker_group_sequence; ALTER TABLE t_ds_relation_project_worker_group ALTER COLUMN id SET DEFAULT NEXTVAL('t_ds_relation_project_worker_group_sequence'); -ALTER TABLE t_ds_project_parameter ADD COLUMN IF NOT EXISTS operator int; \ No newline at end of file +ALTER TABLE t_ds_project_parameter ADD COLUMN IF NOT EXISTS operator int; + +-- modify_data_t_ds_audit_log_input_entry +delimiter d// +CREATE OR REPLACE FUNCTION modify_data_t_ds_audit_log_input_entry() RETURNS void AS $$ +BEGIN + IF EXISTS (SELECT 1 + FROM information_schema.columns + WHERE table_name = 't_ds_audit_log' + AND column_name = 'resource_type') + THEN +ALTER TABLE t_ds_audit_log +drop resource_type, drop operation, drop resource_id, + add model_id bigint NOT NULL, + add model_name VARCHAR(255) NOT NULL, + add model_type VARCHAR(255) NOT NULL, + add operation_type VARCHAR(255) NOT NULL, + add description VARCHAR(255) NOT NULL, + add latency int NOT NULL, + add detail VARCHAR(255) DEFAULT NULL; +END IF; +END; +$$ LANGUAGE plpgsql; +d// + +select modify_data_t_ds_audit_log_input_entry(); +DROP FUNCTION IF EXISTS modify_data_t_ds_audit_log_input_entry(); \ No newline at end of file diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/AuditLogMapperTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/AuditLogMapperTest.java index cc0532032dc2..023b559e79ab 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/AuditLogMapperTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/AuditLogMapperTest.java @@ -17,12 +17,15 @@ package org.apache.dolphinscheduler.dao.mapper; -import org.apache.dolphinscheduler.common.enums.AuditResourceType; +import org.apache.dolphinscheduler.common.enums.AuditModelType; +import org.apache.dolphinscheduler.common.enums.AuditOperationType; import org.apache.dolphinscheduler.dao.BaseDaoTest; import org.apache.dolphinscheduler.dao.entity.AuditLog; import org.apache.dolphinscheduler.dao.entity.Project; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -30,6 +33,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.Lists; public class AuditLogMapperTest extends BaseDaoTest { @@ -39,13 +43,17 @@ public class AuditLogMapperTest extends BaseDaoTest { @Autowired private ProjectMapper projectMapper; - private void insertOne(AuditResourceType resourceType) { + private void insertOne(AuditModelType objectType) { AuditLog auditLog = new AuditLog(); auditLog.setUserId(1); + auditLog.setModelName("name"); + auditLog.setDetail("detail"); + auditLog.setLatency(1L); auditLog.setTime(new Date()); - auditLog.setResourceType(resourceType.getCode()); - auditLog.setOperation(0); - auditLog.setResourceId(0); + auditLog.setModelType(objectType.getName()); + auditLog.setOperationType(AuditOperationType.CREATE.getName()); + auditLog.setModelId(1L); + auditLog.setDescription("description"); logMapper.insert(auditLog); } @@ -65,25 +73,14 @@ private Project insertProject() { */ @Test public void testQueryAuditLog() { - insertOne(AuditResourceType.USER_MODULE); - insertOne(AuditResourceType.PROJECT_MODULE); + insertOne(AuditModelType.USER); + insertOne(AuditModelType.PROJECT); Page page = new Page<>(1, 3); - int[] resourceType = new int[0]; - int[] operationType = new int[0]; + List objectTypeList = new ArrayList<>(); + List operationTypeList = Lists.newArrayList(AuditOperationType.CREATE.getName()); - IPage logIPage = logMapper.queryAuditLog(page, resourceType, operationType, "", null, null); + IPage logIPage = + logMapper.queryAuditLog(page, objectTypeList, operationTypeList, "", "", null, null); Assertions.assertNotEquals(0, logIPage.getTotal()); } - - @Test - public void testQueryResourceNameByType() { - String resourceNameByUser = logMapper.queryResourceNameByType(AuditResourceType.USER_MODULE.getMsg(), 1); - Assertions.assertEquals("admin", resourceNameByUser); - Project project = insertProject(); - String resourceNameByProject = - logMapper.queryResourceNameByType(AuditResourceType.PROJECT_MODULE.getMsg(), project.getId()); - Assertions.assertEquals(project.getName(), resourceNameByProject); - int delete = projectMapper.deleteById(project.getId()); - Assertions.assertEquals(delete, 1); - } } diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java index 2b3dbbebbb98..40191f6aa3b5 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java @@ -1774,6 +1774,7 @@ public int saveProcessDefine(User operator, ProcessDefinition processDefinition, if (Boolean.TRUE.equals(syncDefine)) { if (processDefinition.getId() == null) { result = processDefineMapper.insert(processDefinitionLog); + processDefinition.setId(processDefinitionLog.getId()); } else { processDefinitionLog.setId(processDefinition.getId()); result = processDefineMapper.updateById(processDefinitionLog); diff --git a/dolphinscheduler-ui/src/locales/en_US/monitor.ts b/dolphinscheduler-ui/src/locales/en_US/monitor.ts index 27bd82ce70b2..9a7550177a13 100644 --- a/dolphinscheduler-ui/src/locales/en_US/monitor.ts +++ b/dolphinscheduler-ui/src/locales/en_US/monitor.ts @@ -60,9 +60,11 @@ export default { }, audit_log: { user_name: 'User Name', - resource_type: 'Resource Type', - project_name: 'Project Name', operation_type: 'Operation Type', + model_type: 'Model Type', + model_name: 'Model Name', + latency: 'Latency', + description: 'Description', create_time: 'Create Time', start_time: 'Start Time', end_time: 'End Time', diff --git a/dolphinscheduler-ui/src/locales/zh_CN/monitor.ts b/dolphinscheduler-ui/src/locales/zh_CN/monitor.ts index eca2fe7a206d..dbae52fe074c 100644 --- a/dolphinscheduler-ui/src/locales/zh_CN/monitor.ts +++ b/dolphinscheduler-ui/src/locales/zh_CN/monitor.ts @@ -58,9 +58,11 @@ export default { }, audit_log: { user_name: '用户名称', - resource_type: '资源类型', - project_name: '项目名称', operation_type: '操作类型', + model_type: '模型类型', + model_name: '模型名称', + latency: '耗时', + description: '描述', create_time: '创建时间', start_time: '开始时间', end_time: '结束时间', diff --git a/dolphinscheduler-ui/src/service/modules/audit/index.ts b/dolphinscheduler-ui/src/service/modules/audit/index.ts index fd436ccbb26e..a436fcdbaf82 100644 --- a/dolphinscheduler-ui/src/service/modules/audit/index.ts +++ b/dolphinscheduler-ui/src/service/modules/audit/index.ts @@ -25,3 +25,17 @@ export function queryAuditLogListPaging(params: AuditListReq): any { params }) } + +export function queryAuditModelType(): any { + return axios({ + url: '/projects/audit/audit-log-model-type', + method: 'get' + }) +} + +export function queryAuditLogOperationType(): any { + return axios({ + url: '/projects/audit/audit-log-operation-type', + method: 'get' + }) +} diff --git a/dolphinscheduler-ui/src/service/modules/audit/types.ts b/dolphinscheduler-ui/src/service/modules/audit/types.ts index 030cfc803797..e25efada3194 100644 --- a/dolphinscheduler-ui/src/service/modules/audit/types.ts +++ b/dolphinscheduler-ui/src/service/modules/audit/types.ts @@ -45,4 +45,20 @@ interface AuditListRes { start: number } -export { AuditListReq, AuditListRes } +interface AuditModelTypeItem { + code: number + name: string + child: AuditModelTypeItem[] | null +} + +interface AuditOperationTypeItem { + code: number + name: string +} + +export { + AuditListReq, + AuditListRes, + AuditModelTypeItem, + AuditOperationTypeItem +} diff --git a/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/index.tsx b/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/index.tsx index 64ae7beff0dd..17de81fb51e1 100644 --- a/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/index.tsx +++ b/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/index.tsx @@ -30,7 +30,8 @@ import { NButton, NIcon, NDataTable, - NPagination + NPagination, + NCascader } from 'naive-ui' import { SearchOutlined } from '@vicons/antd' import { useTable } from './use-table' @@ -40,15 +41,23 @@ import Card from '@/components/card' const AuditLog = defineComponent({ name: 'audit-log', setup() { - const { t, variables, getTableData, createColumns } = useTable() + const { + t, + variables, + getTableData, + createColumns, + getModelTypeData, + getOperationTypeData + } = useTable() const requestTableData = () => { getTableData({ pageSize: variables.pageSize, pageNo: variables.page, - resourceType: variables.resourceType, + modelType: variables.modelType, operationType: variables.operationType, userName: variables.userName, + modelName: variables.modelName, datePickerRange: variables.datePickerRange }) } @@ -67,6 +76,8 @@ const AuditLog = defineComponent({ onMounted(() => { createColumns(variables) + getModelTypeData() + getOperationTypeData() requestTableData() }) @@ -97,36 +108,41 @@ const AuditLog = defineComponent({ placeholder={t('monitor.audit_log.user_name')} clearable /> - + + diff --git a/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/use-table.ts b/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/use-table.ts index 87e521503011..e0a17bbc49b1 100644 --- a/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/use-table.ts +++ b/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/use-table.ts @@ -18,22 +18,40 @@ import { useI18n } from 'vue-i18n' import { reactive, ref } from 'vue' import { useAsyncState } from '@vueuse/core' -import { queryAuditLogListPaging } from '@/service/modules/audit' +import { + queryAuditLogListPaging, + queryAuditModelType, + queryAuditLogOperationType +} from '@/service/modules/audit' import { format } from 'date-fns' import { parseTime } from '@/common/common' -import type { AuditListRes } from '@/service/modules/audit/types' +import type { + AuditListRes, + AuditModelTypeItem, + AuditOperationTypeItem +} from '@/service/modules/audit/types' +import { + COLUMN_WIDTH_CONFIG, + calculateTableWidth, + DefaultTableWidth +} from '@/common/column-width-config' +import { sortBy } from 'lodash' export function useTable() { const { t } = useI18n() const variables = reactive({ columns: [], + tableWidth: DefaultTableWidth, tableData: [], page: ref(1), pageSize: ref(10), - resourceType: ref(null), + modelType: ref(null), operationType: ref(null), + ModelTypeData: [], + OperationTypeData: [], userName: ref(null), + modelName: ref(null), datePickerRange: ref(null), totalPage: ref(1), loadingRef: ref(false) @@ -44,29 +62,70 @@ export function useTable() { { title: '#', key: 'index', - render: (row: any, index: number) => index + 1 + render: (row: any, index: number) => index + 1, + ...COLUMN_WIDTH_CONFIG['index'] }, { title: t('monitor.audit_log.user_name'), - key: 'userName' + key: 'userName', + ...COLUMN_WIDTH_CONFIG['userName'] }, { - title: t('monitor.audit_log.resource_type'), - key: 'resource' + title: t('monitor.audit_log.model_type'), + key: 'modelType', + ...COLUMN_WIDTH_CONFIG['type'] }, { - title: t('monitor.audit_log.project_name'), - key: 'resourceName' + title: t('monitor.audit_log.model_name'), + key: 'modelName', + ...COLUMN_WIDTH_CONFIG['name'] }, { title: t('monitor.audit_log.operation_type'), - key: 'operation' + key: 'operation', + ...COLUMN_WIDTH_CONFIG['type'] + }, + + { + title: t('monitor.audit_log.description'), + key: 'description', + ...COLUMN_WIDTH_CONFIG['note'] + }, + { + title: t('monitor.audit_log.latency') + ' (ms)', + key: 'latency', + ...COLUMN_WIDTH_CONFIG['times'] }, { title: t('monitor.audit_log.create_time'), - key: 'time' + key: 'time', + ...COLUMN_WIDTH_CONFIG['time'] } ] + + if (variables.tableWidth) { + variables.tableWidth = calculateTableWidth(variables.columns) + } + } + + const getModelTypeData = async () => { + try { + variables.ModelTypeData = await queryAuditModelType().then( + (res: AuditModelTypeItem[]) => res || [] + ) + } catch { + variables.ModelTypeData = [] + } + } + + const getOperationTypeData = async () => { + try { + variables.OperationTypeData = await queryAuditLogOperationType().then( + (res: AuditOperationTypeItem[]) => sortBy(res, 'name') + ) + } catch { + variables.OperationTypeData = [] + } } const getTableData = (params: any) => { @@ -75,9 +134,10 @@ export function useTable() { const data = { pageSize: params.pageSize, pageNo: params.pageNo, - resourceType: params.resourceType, - operationType: params.operationType, + modelTypes: params.modelType, + operationTypes: params.operationType, userName: params.userName, + modelName: params.modelName, startDate: params.datePickerRange ? format(parseTime(params.datePickerRange[0]), 'yyyy-MM-dd HH:mm:ss') : '', @@ -106,6 +166,8 @@ export function useTable() { t, variables, getTableData, - createColumns + createColumns, + getModelTypeData: getModelTypeData, + getOperationTypeData } } From 7894ebb3506d4281c27715f684e9fb5701bf5351 Mon Sep 17 00:00:00 2001 From: Evan Sun Date: Mon, 15 Apr 2024 14:15:44 +0800 Subject: [PATCH 026/165] [TEST] increase coverage of environment service (#15840) * [TEST] increase coverage of environment service * spotless apply --- .../service/impl/EnvironmentServiceTest.java | 88 ++++++++++++++++++- .../api/utils/ServiceTestUtil.java | 30 +++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/ServiceTestUtil.java diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceTest.java index 14e1ed956b47..b226295e0e4e 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceTest.java @@ -22,6 +22,7 @@ import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ENVIRONMENT_CREATE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ENVIRONMENT_DELETE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.ENVIRONMENT_UPDATE; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -29,9 +30,11 @@ import org.apache.dolphinscheduler.api.permission.ResourcePermissionCheckService; import org.apache.dolphinscheduler.api.utils.PageInfo; import org.apache.dolphinscheduler.api.utils.Result; +import org.apache.dolphinscheduler.api.utils.ServiceTestUtil; import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.enums.AuthorizationType; import org.apache.dolphinscheduler.common.enums.UserType; +import org.apache.dolphinscheduler.common.utils.CodeGenerateUtils; import org.apache.dolphinscheduler.dao.entity.Environment; import org.apache.dolphinscheduler.dao.entity.EnvironmentWorkerGroupRelation; import org.apache.dolphinscheduler.dao.entity.User; @@ -42,6 +45,7 @@ import org.apache.commons.collections4.CollectionUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -53,6 +57,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -91,6 +96,9 @@ public class EnvironmentServiceTest { @Mock private ResourcePermissionCheckService resourcePermissionCheckService; + @Mock + private CodeGenerateUtils codeGenerateUtils; + public static final String testUserName = "environmentServerTest"; public static final String environmentName = "Env1"; @@ -121,9 +129,24 @@ public void testCreateEnvironment() { when(environmentMapper.insert(any(Environment.class))).thenReturn(1); when(relationMapper.insert(any(EnvironmentWorkerGroupRelation.class))).thenReturn(1); + + assertThrowsServiceException(Status.DESCRIPTION_TOO_LONG_ERROR, + () -> environmentService.createEnvironment(adminUser, "testName", "test", + ServiceTestUtil.randomStringWithLengthN(512), workerGroups)); assertDoesNotThrow( () -> environmentService.createEnvironment(adminUser, "testName", "test", "test", workerGroups)); + when(environmentMapper.insert(any(Environment.class))).thenReturn(-1); + assertThrowsServiceException(Status.CREATE_ENVIRONMENT_ERROR, + () -> environmentService.createEnvironment(adminUser, "testName", "test", "test", workerGroups)); + + try (MockedStatic ignored = Mockito.mockStatic(CodeGenerateUtils.class)) { + when(CodeGenerateUtils.getInstance()).thenReturn(codeGenerateUtils); + when(codeGenerateUtils.genCode()).thenThrow(CodeGenerateUtils.CodeGenerateException.class); + + assertThrowsServiceException(Status.INTERNAL_SERVER_ERROR_ARGS, + () -> environmentService.createEnvironment(adminUser, "testName", "test", "test", workerGroups)); + } } @Test @@ -156,25 +179,54 @@ public void testUpdateEnvironmentByCode() { assertThrowsServiceException(Status.ENVIRONMENT_NAME_EXISTS, () -> environmentService .updateEnvironmentByCode(adminUser, 2L, environmentName, getConfig(), getDesc(), workerGroups)); + when(environmentMapper.update(any(Environment.class), any(Wrapper.class))).thenReturn(-1); + assertThrowsServiceException(Status.UPDATE_ENVIRONMENT_ERROR, + () -> environmentService.updateEnvironmentByCode(adminUser, 1L, "testName", "test", "test", + workerGroups)); + when(environmentMapper.update(any(Environment.class), any(Wrapper.class))).thenReturn(1); + + assertThrowsServiceException(Status.DESCRIPTION_TOO_LONG_ERROR, + () -> environmentService.updateEnvironmentByCode(adminUser, 2L, environmentName, getConfig(), + ServiceTestUtil.randomStringWithLengthN(512), workerGroups)); + assertDoesNotThrow(() -> environmentService.updateEnvironmentByCode(adminUser, 1L, "testName", "test", "test", workerGroups)); + + assertDoesNotThrow(() -> environmentService.updateEnvironmentByCode(adminUser, 1L, "testName", "test", "test", + "")); + + when(relationMapper.queryByEnvironmentCode(any())) + .thenReturn(Collections.singletonList(getEnvironmentWorkerGroup())); + assertDoesNotThrow(() -> environmentService.updateEnvironmentByCode(adminUser, 1L, "testName", "test", "test", + "")); } @Test public void testQueryAllEnvironmentList() { + when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.ENVIRONMENT, + 1, environmentServiceLogger)).thenReturn(Collections.emptySet()); + Map result = environmentService.queryAllEnvironmentList(getAdminUser()); + assertEquals(0, ((List) result.get(Constants.DATA_LIST)).size()); + Set ids = new HashSet<>(); ids.add(1); when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.ENVIRONMENT, 1, environmentServiceLogger)).thenReturn(ids); when(environmentMapper.selectBatchIds(ids)).thenReturn(Lists.newArrayList(getEnvironment())); - Map result = environmentService.queryAllEnvironmentList(getAdminUser()); + result = environmentService.queryAllEnvironmentList(getAdminUser()); logger.info(result.toString()); Assertions.assertEquals(Status.SUCCESS, result.get(Constants.STATUS)); List list = (List) (result.get(Constants.DATA_LIST)); Assertions.assertEquals(1, list.size()); + + when(environmentMapper.selectBatchIds(ids)).thenReturn(Collections.emptyList()); + result = environmentService.queryAllEnvironmentList(getAdminUser()); + Assertions.assertEquals(Status.SUCCESS, result.get(Constants.STATUS)); + list = (List) (result.get(Constants.DATA_LIST)); + Assertions.assertEquals(0, list.size()); } @Test @@ -186,9 +238,27 @@ public void testQueryEnvironmentListPaging() { .thenReturn(page); Result result = environmentService.queryEnvironmentListPaging(getAdminUser(), 1, 10, environmentName); - logger.info(result.toString()); PageInfo pageInfo = (PageInfo) result.getData(); Assertions.assertTrue(CollectionUtils.isNotEmpty(pageInfo.getTotalList())); + + assertDoesNotThrow( + () -> environmentService.queryEnvironmentListPaging(getGeneralUser(), 1, 10, environmentName)); + + when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition( + AuthorizationType.ENVIRONMENT, + 1, + environmentServiceLogger)).thenReturn(Collections.singleton(10)); + when(environmentMapper.queryEnvironmentListPagingByIds(any(Page.class), any(List.class), any(String.class))) + .thenReturn(page); + result = environmentService.queryEnvironmentListPaging(getGeneralUser(), 1, 10, environmentName); + assertEquals(0, result.getCode()); + assertEquals(1, ((PageInfo) result.getData()).getTotalList().size()); + + page.setRecords(Collections.emptyList()); + page.setTotal(0); + result = environmentService.queryEnvironmentListPaging(getGeneralUser(), 1, 10, environmentName); + assertEquals(0, result.getCode()); + assertEquals(0, ((PageInfo) result.getData()).getTotalList().size()); } @Test @@ -239,6 +309,10 @@ public void testDeleteEnvironmentByCode() { result = environmentService.deleteEnvironmentByCode(loginUser, 1L); logger.info(result.toString()); Assertions.assertEquals(Status.SUCCESS, result.get(Constants.STATUS)); + + when(environmentMapper.deleteByCode(1L)).thenReturn(-1); + result = environmentService.deleteEnvironmentByCode(loginUser, 1L); + Assertions.assertEquals(Status.DELETE_ENVIRONMENT_ERROR, result.get(Constants.STATUS)); } @Test @@ -251,6 +325,9 @@ public void testVerifyEnvironment() { result = environmentService.verifyEnvironment(environmentName); logger.info(result.toString()); Assertions.assertEquals(Status.ENVIRONMENT_NAME_EXISTS, result.get(Constants.STATUS)); + + when(environmentMapper.queryByEnvironmentName(environmentName)).thenReturn(null); + assertDoesNotThrow(() -> environmentService.verifyEnvironment(environmentName)); } private Environment getEnvironment() { @@ -264,6 +341,13 @@ private Environment getEnvironment() { return environment; } + private EnvironmentWorkerGroupRelation getEnvironmentWorkerGroup() { + EnvironmentWorkerGroupRelation relation = new EnvironmentWorkerGroupRelation(); + relation.setEnvironmentCode(1L); + relation.setWorkerGroup("new_worker_group"); + return relation; + } + /** * create an environment description */ diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/ServiceTestUtil.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/ServiceTestUtil.java new file mode 100644 index 000000000000..0dc71841b439 --- /dev/null +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/ServiceTestUtil.java @@ -0,0 +1,30 @@ +/* + * 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.utils; + +import java.nio.charset.StandardCharsets; +import java.util.Random; + +public class ServiceTestUtil { + + public static String randomStringWithLengthN(int n) { + byte[] bitArray = new byte[n]; + new Random().nextBytes(bitArray); + return new String(bitArray, StandardCharsets.UTF_8); + } +} From 5ad7b1509f79c52b24d11f50c1d0172287dcd968 Mon Sep 17 00:00:00 2001 From: XinXing <49302071+xinxingi@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:01:28 +0800 Subject: [PATCH 027/165] =?UTF-8?q?[Fix-15787]=20Reuse=20code=20and=20solv?= =?UTF-8?q?e=20the=20problem=20of=20complex=20SQL=20parsing=20exceptions?= =?UTF-8?q?=20in=E2=80=A6=20(#15833)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reuse code and solve the problem of complex SQL parsing exceptions in druid, corresponding to issue #15787 * Code Format * Enhanced adaptability to SQL formatting --- .../oracle/param/OracleDataSourceProcessor.java | 11 ++++++----- .../oracle/param/OracleDataSourceProcessorTest.java | 13 ++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oracle/param/OracleDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oracle/param/OracleDataSourceProcessor.java index 89b872d7f595..5f24b8cc1939 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oracle/param/OracleDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oracle/param/OracleDataSourceProcessor.java @@ -41,8 +41,7 @@ import com.alibaba.druid.sql.ast.SQLStatement; import com.alibaba.druid.sql.dialect.oracle.parser.OracleStatementParser; -import com.alibaba.druid.sql.parser.SQLParserFeature; -import com.alibaba.druid.sql.parser.SQLStatementParser; +import com.alibaba.druid.sql.parser.SQLParserUtils; import com.google.auto.service.AutoService; @AutoService(DataSourceProcessor.class) @@ -149,9 +148,11 @@ public DataSourceProcessor create() { @Override public List splitAndRemoveComment(String sql) { - SQLStatementParser parser = new OracleStatementParser(sql, SQLParserFeature.KeepComments); - List statementList = parser.parseStatementList(); - return statementList.stream().map(SQLStatement::toString).collect(Collectors.toList()); + if (sql.toUpperCase().contains("BEGIN") && sql.toUpperCase().contains("END")) { + return new OracleStatementParser(sql).parseStatementList().stream().map(SQLStatement::toString) + .collect(Collectors.toList()); + } + return SQLParserUtils.splitAndRemoveComment(sql, com.alibaba.druid.DbType.oracle); } private String transformOther(Map otherMap) { diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/test/java/org/apache/dolphinscheduler/plugin/datasource/oracle/param/OracleDataSourceProcessorTest.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/test/java/org/apache/dolphinscheduler/plugin/datasource/oracle/param/OracleDataSourceProcessorTest.java index 57e316ed6365..2f9133463ded 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/test/java/org/apache/dolphinscheduler/plugin/datasource/oracle/param/OracleDataSourceProcessorTest.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/test/java/org/apache/dolphinscheduler/plugin/datasource/oracle/param/OracleDataSourceProcessorTest.java @@ -155,13 +155,16 @@ void splitAndRemoveComment_PLSQLWithComment() { @Test void splitAndRemoveComment_MultipleSql() { - String plSql = "select * from test;select * from test2;"; + String plSql = + "select a,a-a as b from (select 1 as a,2 as b from dual) union all select 1 as a,2 as b from dual;select * from dual; -- this comment"; List sqls = oracleDatasourceProcessor.splitAndRemoveComment(plSql); // We will not split the plsql Assertions.assertEquals(2, sqls.size()); - Assertions.assertEquals("SELECT *\n" + - "FROM test;", sqls.get(0)); - Assertions.assertEquals("SELECT *\n" + - "FROM test2;", sqls.get(1)); + System.out.println(sqls.get(0)); + System.out.println(sqls.get(1)); + Assertions.assertEquals( + "select a,a-a as b from (select 1 as a,2 as b from dual) union all select 1 as a,2 as b from dual", + sqls.get(0)); + Assertions.assertEquals("select * from dual", sqls.get(1)); } } From 6844c659bf44f7f027adbe7a64723a1ca0f1cd89 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Mon, 15 Apr 2024 16:14:32 +0800 Subject: [PATCH 028/165] Fix ErrorCommand loss some fields in Command (#15847) --- .../dao/entity/ErrorCommand.java | 17 +++- .../dao/entity/ErrorCommandTest.java | 82 +++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/entity/ErrorCommandTest.java diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ErrorCommand.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ErrorCommand.java index aa4d3d47821a..d52984b61fde 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ErrorCommand.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ErrorCommand.java @@ -52,6 +52,10 @@ public class ErrorCommand { */ private long processDefinitionCode; + private int processDefinitionVersion; + + private int processInstanceId; + /** * executor id */ @@ -73,7 +77,7 @@ public class ErrorCommand { private FailureStrategy failureStrategy; /** - * warning type + * warning type */ private WarningType warningType; @@ -135,21 +139,26 @@ public class ErrorCommand { public ErrorCommand() { } + public ErrorCommand(Command command, String message) { this.id = command.getId(); this.commandType = command.getCommandType(); this.executorId = command.getExecutorId(); this.processDefinitionCode = command.getProcessDefinitionCode(); + this.processDefinitionVersion = command.getProcessDefinitionVersion(); + this.processInstanceId = command.getProcessInstanceId(); this.commandParam = command.getCommandParam(); + this.taskDependType = command.getTaskDependType(); + this.failureStrategy = command.getFailureStrategy(); this.warningType = command.getWarningType(); this.warningGroupId = command.getWarningGroupId(); this.scheduleTime = command.getScheduleTime(); - this.taskDependType = command.getTaskDependType(); - this.failureStrategy = command.getFailureStrategy(); this.startTime = command.getStartTime(); this.updateTime = command.getUpdateTime(); - this.environmentCode = command.getEnvironmentCode(); this.processInstancePriority = command.getProcessInstancePriority(); + this.workerGroup = command.getWorkerGroup(); + this.tenantCode = command.getTenantCode(); + this.environmentCode = command.getEnvironmentCode(); this.message = message; this.dryRun = command.getDryRun(); this.testFlag = command.getTestFlag(); diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/entity/ErrorCommandTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/entity/ErrorCommandTest.java new file mode 100644 index 000000000000..057d6578e6a4 --- /dev/null +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/entity/ErrorCommandTest.java @@ -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. + */ + +package org.apache.dolphinscheduler.dao.entity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.dolphinscheduler.common.enums.CommandType; +import org.apache.dolphinscheduler.common.enums.FailureStrategy; +import org.apache.dolphinscheduler.common.enums.Flag; +import org.apache.dolphinscheduler.common.enums.Priority; +import org.apache.dolphinscheduler.common.enums.TaskDependType; +import org.apache.dolphinscheduler.common.enums.WarningType; + +import java.util.Date; + +import org.junit.jupiter.api.Test; + +class ErrorCommandTest { + + @Test + void testConstructor() { + Command command = new Command(); + command.setId(1); + command.setCommandType(CommandType.PAUSE); + command.setExecutorId(1); + command.setProcessDefinitionCode(123); + command.setProcessDefinitionVersion(1); + command.setProcessInstanceId(1); + command.setCommandParam("param"); + command.setTaskDependType(TaskDependType.TASK_POST); + command.setFailureStrategy(FailureStrategy.CONTINUE); + command.setWarningType(WarningType.ALL); + command.setWarningGroupId(1); + command.setScheduleTime(new Date()); + command.setStartTime(new Date()); + command.setUpdateTime(new Date()); + command.setProcessInstancePriority(Priority.HIGHEST); + command.setWorkerGroup("default"); + command.setTenantCode("root"); + command.setEnvironmentCode(1L); + command.setDryRun(1); + command.setTestFlag(Flag.NO.getCode()); + + ErrorCommand errorCommand = new ErrorCommand(command, "test"); + assertEquals(command.getCommandType(), errorCommand.getCommandType()); + assertEquals(command.getExecutorId(), errorCommand.getExecutorId()); + assertEquals(command.getProcessDefinitionCode(), errorCommand.getProcessDefinitionCode()); + assertEquals(command.getProcessDefinitionVersion(), errorCommand.getProcessDefinitionVersion()); + assertEquals(command.getProcessInstanceId(), errorCommand.getProcessInstanceId()); + assertEquals(command.getCommandParam(), errorCommand.getCommandParam()); + assertEquals(command.getTaskDependType(), errorCommand.getTaskDependType()); + assertEquals(command.getFailureStrategy(), errorCommand.getFailureStrategy()); + assertEquals(command.getWarningType(), errorCommand.getWarningType()); + assertEquals(command.getWarningGroupId(), errorCommand.getWarningGroupId()); + assertEquals(command.getScheduleTime(), errorCommand.getScheduleTime()); + assertEquals(command.getStartTime(), errorCommand.getStartTime()); + assertEquals(command.getUpdateTime(), errorCommand.getUpdateTime()); + assertEquals(command.getProcessInstancePriority(), errorCommand.getProcessInstancePriority()); + assertEquals(command.getWorkerGroup(), errorCommand.getWorkerGroup()); + assertEquals(command.getTenantCode(), errorCommand.getTenantCode()); + assertEquals(command.getEnvironmentCode(), errorCommand.getEnvironmentCode()); + assertEquals(command.getDryRun(), errorCommand.getDryRun()); + assertEquals(command.getTestFlag(), errorCommand.getTestFlag()); + assertEquals("test", errorCommand.getMessage()); + } + +} From f99d3f1ed37f1f36a51cfa2063d298f1bed5de49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=BA=E9=98=B3?= Date: Mon, 15 Apr 2024 17:01:06 +0800 Subject: [PATCH 029/165] [Improvement][Audit] Change time to create_time (#15846) * change time to create_time * update * update --- .../api/audit/OperatorUtils.java | 2 +- .../dolphinscheduler/api/dto/AuditDto.java | 2 +- .../api/service/impl/AuditServiceImpl.java | 2 +- .../dolphinscheduler/dao/entity/AuditLog.java | 2 +- .../dao/mapper/AuditLogMapper.xml | 8 ++++---- .../resources/sql/dolphinscheduler_h2.sql | 2 +- .../resources/sql/dolphinscheduler_mysql.sql | 2 +- .../sql/dolphinscheduler_postgresql.sql | 2 +- .../mysql/dolphinscheduler_ddl.sql | 2 +- .../postgresql/dolphinscheduler_ddl.sql | 19 ++++++++++--------- .../dao/mapper/AuditLogMapperTest.java | 2 +- .../monitor/statistics/audit-log/use-table.ts | 2 +- 12 files changed, 24 insertions(+), 23 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java index e8b6b1254c34..a0233c7e93e3 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java @@ -65,7 +65,7 @@ public static List buildAuditLogList(String apiDescription, AuditType auditLog.setModelType(auditType.getAuditModelType().getName()); auditLog.setOperationType(auditType.getAuditOperationType().getName()); auditLog.setDescription(apiDescription); - auditLog.setTime(new Date()); + auditLog.setCreateTime(new Date()); auditLogList.add(auditLog); return auditLogList; diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/AuditDto.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/AuditDto.java index 2c4144220bdb..0b36325b44c0 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/AuditDto.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/AuditDto.java @@ -34,7 +34,7 @@ public class AuditDto { private String operation; - private Date time; + private Date createTime; private String description; diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AuditServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AuditServiceImpl.java index bdc7a4c573a6..2ec547bcb644 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AuditServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AuditServiceImpl.java @@ -129,7 +129,7 @@ private AuditDto transformAuditLog(AuditLog auditLog) { auditDto.setLatency(String.valueOf(auditLog.getLatency())); auditDto.setDetail(auditLog.getDetail()); auditDto.setDescription(auditLog.getDescription()); - auditDto.setTime(auditLog.getTime()); + auditDto.setCreateTime(auditLog.getCreateTime()); return auditDto; } } diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/AuditLog.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/AuditLog.java index 397fa722e533..7c6f76e1da15 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/AuditLog.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/AuditLog.java @@ -67,7 +67,7 @@ public class AuditLog { /** * operation time */ - private Date time; + private Date createTime; private String detail; diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AuditLogMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AuditLogMapper.xml index 94430a343a42..63adb57c3402 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AuditLogMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AuditLogMapper.xml @@ -19,10 +19,10 @@ - id, user_id, model_type, operation_type, model_id, model_name, time, detail, description, latency + id, user_id, model_type, operation_type, model_id, model_name, create_time, detail, description, latency - ${alias}.id, ${alias}.user_id, ${alias}.model_type, ${alias}.operation_type, ${alias}.model_id, ${alias}.model_name, ${alias}.time, ${alias}.detail, ${alias}.description, ${alias}.latency + ${alias}.id, ${alias}.user_id, ${alias}.model_type, ${alias}.operation_type, ${alias}.model_id, ${alias}.model_name, ${alias}.create_time, ${alias}.detail, ${alias}.description, ${alias}.latency diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql index 4187b93a1c87..42c893bb0532 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql @@ -2027,7 +2027,7 @@ CREATE TABLE t_ds_audit_log description varchar(255) NOT NULL, latency int(11) NOT NULL, detail varchar(255) DEFAULT NULL, - time timestamp NULL DEFAULT CURRENT_TIMESTAMP, + create_time timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ); diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql index f3d01c14b5db..e2138b67a2c1 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql @@ -2016,7 +2016,7 @@ CREATE TABLE `t_ds_audit_log` ( `description` varchar(100) DEFAULT NULL COMMENT 'api description', `latency` int(11) DEFAULT NULL COMMENT 'api cost milliseconds', `detail` varchar(100) DEFAULT NULL COMMENT 'object change detail', - `time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'operation time', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'operation time', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET=utf8 COLLATE = utf8_bin; diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql index 5dd804c0c791..2d224092c44f 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql @@ -2001,7 +2001,7 @@ CREATE TABLE t_ds_audit_log ( description VARCHAR(255) NOT NULL, latency int NOT NULL, detail VARCHAR(255) DEFAULT NULL, - time timestamp DEFAULT NULL , + create_time timestamp DEFAULT NULL , PRIMARY KEY (id) ); diff --git a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/mysql/dolphinscheduler_ddl.sql b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/mysql/dolphinscheduler_ddl.sql index b5e5b31d113d..d10ac6b71052 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/mysql/dolphinscheduler_ddl.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/mysql/dolphinscheduler_ddl.sql @@ -47,7 +47,7 @@ drop resource_type, drop operation, drop resource_id, add `description` varchar(100) DEFAULT NULL COMMENT 'api description', add `latency` int(11) DEFAULT NULL COMMENT 'api cost milliseconds', add `detail` varchar(100) DEFAULT NULL COMMENT 'object change detail', - MODIFY COLUMN `time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT "operation time"; + CHANGE COLUMN `time` `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT "operation time"; END IF; END; d// diff --git a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/postgresql/dolphinscheduler_ddl.sql b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/postgresql/dolphinscheduler_ddl.sql index 22e1c599de96..ee06588e58a1 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/postgresql/dolphinscheduler_ddl.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.2.2_schema/postgresql/dolphinscheduler_ddl.sql @@ -40,15 +40,16 @@ BEGIN WHERE table_name = 't_ds_audit_log' AND column_name = 'resource_type') THEN -ALTER TABLE t_ds_audit_log -drop resource_type, drop operation, drop resource_id, - add model_id bigint NOT NULL, - add model_name VARCHAR(255) NOT NULL, - add model_type VARCHAR(255) NOT NULL, - add operation_type VARCHAR(255) NOT NULL, - add description VARCHAR(255) NOT NULL, - add latency int NOT NULL, - add detail VARCHAR(255) DEFAULT NULL; + ALTER TABLE t_ds_audit_log + drop resource_type, drop operation, drop resource_id, + add model_id bigint NOT NULL, + add model_name VARCHAR(255) NOT NULL, + add model_type VARCHAR(255) NOT NULL, + add operation_type VARCHAR(255) NOT NULL, + add description VARCHAR(255) NOT NULL, + add latency int NOT NULL, + add detail VARCHAR(255) DEFAULT NULL; + ALTER TABLE t_ds_audit_log RENAME COLUMN "time" TO "create_time"; END IF; END; $$ LANGUAGE plpgsql; diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/AuditLogMapperTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/AuditLogMapperTest.java index 023b559e79ab..92418193b84b 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/AuditLogMapperTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/AuditLogMapperTest.java @@ -49,7 +49,7 @@ private void insertOne(AuditModelType objectType) { auditLog.setModelName("name"); auditLog.setDetail("detail"); auditLog.setLatency(1L); - auditLog.setTime(new Date()); + auditLog.setCreateTime(new Date()); auditLog.setModelType(objectType.getName()); auditLog.setOperationType(AuditOperationType.CREATE.getName()); auditLog.setModelId(1L); diff --git a/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/use-table.ts b/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/use-table.ts index e0a17bbc49b1..385299d019f0 100644 --- a/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/use-table.ts +++ b/dolphinscheduler-ui/src/views/monitor/statistics/audit-log/use-table.ts @@ -98,7 +98,7 @@ export function useTable() { }, { title: t('monitor.audit_log.create_time'), - key: 'time', + key: 'createTime', ...COLUMN_WIDTH_CONFIG['time'] } ] From 27d0563fe4c020a771448830309429592c66ba9c Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Tue, 16 Apr 2024 10:58:43 +0800 Subject: [PATCH 030/165] Bind processId to constructor CodeGenerator (#15848) --- .../api/python/PythonGateway.java | 6 +- .../api/service/impl/ClusterServiceImpl.java | 2 +- .../service/impl/EnvironmentServiceImpl.java | 2 +- .../api/service/impl/ExecutorServiceImpl.java | 2 +- .../service/impl/K8SNamespaceServiceImpl.java | 2 +- .../impl/ProcessDefinitionServiceImpl.java | 16 ++-- .../impl/ProjectParameterServiceImpl.java | 2 +- .../impl/ProjectPreferenceServiceImpl.java | 2 +- .../api/service/impl/ProjectServiceImpl.java | 2 +- .../impl/TaskDefinitionServiceImpl.java | 6 +- .../service/impl/EnvironmentServiceTest.java | 6 +- .../common/utils/CodeGenerateUtils.java | 93 ++++++++++--------- .../common/utils/CodeGenerateUtilsTest.java | 53 +++++++++-- .../service/process/ProcessServiceImpl.java | 2 +- .../datasource/dao/ProcessDefinitionDao.java | 2 +- .../tools/datasource/dao/ProjectDao.java | 2 +- .../v200/V200DolphinSchedulerUpgrader.java | 2 +- .../tools/demo/ProcessDefinitionDemo.java | 16 ++-- 18 files changed, 130 insertions(+), 88 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/python/PythonGateway.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/python/PythonGateway.java index 1e4e1f5aa099..762ba576b0a8 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/python/PythonGateway.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/python/PythonGateway.java @@ -184,7 +184,7 @@ public Map getCodeAndVersion(String projectName, String processDef Map result = new HashMap<>(); // project do not exists, mean task not exists too, so we should directly return init value if (project == null) { - result.put("code", CodeGenerateUtils.getInstance().genCode()); + result.put("code", CodeGenerateUtils.genCode()); result.put("version", 0L); return result; } @@ -194,7 +194,7 @@ public Map getCodeAndVersion(String projectName, String processDef // In the case project exists, but current workflow still not created, we should also return the init // version of it if (processDefinition == null) { - result.put("code", CodeGenerateUtils.getInstance().genCode()); + result.put("code", CodeGenerateUtils.genCode()); result.put("version", 0L); return result; } @@ -202,7 +202,7 @@ public Map getCodeAndVersion(String projectName, String processDef TaskDefinition taskDefinition = taskDefinitionMapper.queryByName(project.getCode(), processDefinition.getCode(), taskName); if (taskDefinition == null) { - result.put("code", CodeGenerateUtils.getInstance().genCode()); + result.put("code", CodeGenerateUtils.genCode()); result.put("version", 0L); } else { result.put("code", taskDefinition.getCode()); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ClusterServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ClusterServiceImpl.java index a5ceb92abc2f..bf4e3ac4d605 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ClusterServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ClusterServiceImpl.java @@ -96,7 +96,7 @@ public Long createCluster(User loginUser, String name, String config, String des cluster.setOperator(loginUser.getId()); cluster.setCreateTime(new Date()); cluster.setUpdateTime(new Date()); - cluster.setCode(CodeGenerateUtils.getInstance().genCode()); + cluster.setCode(CodeGenerateUtils.genCode()); if (clusterMapper.insert(cluster) > 0) { return cluster.getCode(); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceImpl.java index ade9aea48383..2eb8f84810ea 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceImpl.java @@ -123,7 +123,7 @@ public Long createEnvironment(User loginUser, env.setUpdateTime(new Date()); long code = 0L; try { - code = CodeGenerateUtils.getInstance().genCode(); + code = CodeGenerateUtils.genCode(); env.setCode(code); } catch (CodeGenerateException e) { log.error("Generate environment code error.", e); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExecutorServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExecutorServiceImpl.java index 7ab6102ff8e7..5b576ce95a43 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExecutorServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExecutorServiceImpl.java @@ -258,7 +258,7 @@ public Map execProcessInstance(User loginUser, long projectCode, checkScheduleTimeNumExceed(commandType, cronTime); checkMasterExists(); - long triggerCode = CodeGenerateUtils.getInstance().genCode(); + long triggerCode = CodeGenerateUtils.genCode(); /** * create command diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/K8SNamespaceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/K8SNamespaceServiceImpl.java index 54894a5ca75c..7543616f31c9 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/K8SNamespaceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/K8SNamespaceServiceImpl.java @@ -141,7 +141,7 @@ public Map registerK8sNamespace(User loginUser, String namespace long code = 0L; try { - code = CodeGenerateUtils.getInstance().genCode(); + code = CodeGenerateUtils.genCode(); cluster.setCode(code); } catch (CodeGenerateUtils.CodeGenerateException e) { log.error("Generate cluster code error.", e); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessDefinitionServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessDefinitionServiceImpl.java index ed6b2d27bbc4..2bf84a0bccb2 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessDefinitionServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessDefinitionServiceImpl.java @@ -299,7 +299,7 @@ public Map createProcessDefinition(User loginUser, List taskDefinitionLogs = generateTaskDefinitionList(taskDefinitionJson); List taskRelationList = generateTaskRelationList(taskRelationJson, taskDefinitionLogs); - long processDefinitionCode = CodeGenerateUtils.getInstance().genCode(); + long processDefinitionCode = CodeGenerateUtils.genCode(); ProcessDefinition processDefinition = new ProcessDefinition(projectCode, name, processDefinitionCode, description, globalParams, locations, timeout, loginUser.getId()); @@ -360,7 +360,7 @@ public ProcessDefinition createSingleProcessDefinition(User loginUser, long processDefinitionCode; try { - processDefinitionCode = CodeGenerateUtils.getInstance().genCode(); + processDefinitionCode = CodeGenerateUtils.genCode(); } catch (CodeGenerateException e) { throw new ServiceException(Status.INTERNAL_SERVER_ERROR_ARGS); } @@ -1233,7 +1233,7 @@ public Map importSqlProcessDefinition(User loginUser, long proje // build process definition processDefinition = new ProcessDefinition(projectCode, processDefinitionName, - CodeGenerateUtils.getInstance().genCode(), + CodeGenerateUtils.genCode(), "", "[]", null, 0, loginUser.getId()); @@ -1388,7 +1388,7 @@ private TaskDefinitionLog buildNormalSqlTaskDefinition(String taskName, DataSour sqlParameters.setSqlType(SqlType.NON_QUERY.ordinal()); sqlParameters.setLocalParams(Collections.emptyList()); taskDefinition.setTaskParams(JSONUtils.toJsonString(sqlParameters)); - taskDefinition.setCode(CodeGenerateUtils.getInstance().genCode()); + taskDefinition.setCode(CodeGenerateUtils.genCode()); taskDefinition.setTaskType(TASK_TYPE_SQL); taskDefinition.setFailRetryTimes(0); taskDefinition.setFailRetryInterval(0); @@ -1433,7 +1433,7 @@ protected boolean checkAndImport(User loginUser, processDefinition.setProjectCode(projectCode); processDefinition.setUserId(loginUser.getId()); try { - processDefinition.setCode(CodeGenerateUtils.getInstance().genCode()); + processDefinition.setCode(CodeGenerateUtils.genCode()); } catch (CodeGenerateException e) { log.error( "Save process definition error because generate process definition code error, projectCode:{}.", @@ -1456,7 +1456,7 @@ protected boolean checkAndImport(User loginUser, taskDefinitionLog.setOperator(loginUser.getId()); taskDefinitionLog.setOperateTime(now); try { - long code = CodeGenerateUtils.getInstance().genCode(); + long code = CodeGenerateUtils.genCode(); taskCodeMap.put(taskDefinitionLog.getCode(), code); taskDefinitionLog.setCode(code); } catch (CodeGenerateException e) { @@ -2074,7 +2074,7 @@ protected void doBatchOperateProcessDefinition(User loginUser, Map taskCodeMap = new HashMap<>(); for (TaskDefinitionLog taskDefinitionLog : taskDefinitionLogs) { try { - long taskCode = CodeGenerateUtils.getInstance().genCode(); + long taskCode = CodeGenerateUtils.genCode(); taskCodeMap.put(taskDefinitionLog.getCode(), taskCode); taskDefinitionLog.setCode(taskCode); } catch (CodeGenerateException e) { @@ -2097,7 +2097,7 @@ protected void doBatchOperateProcessDefinition(User loginUser, } final long oldProcessDefinitionCode = processDefinition.getCode(); try { - processDefinition.setCode(CodeGenerateUtils.getInstance().genCode()); + processDefinition.setCode(CodeGenerateUtils.genCode()); } catch (CodeGenerateException e) { log.error("Generate process definition code error, projectCode:{}.", targetProjectCode, e); putMsg(result, Status.INTERNAL_SERVER_ERROR_ARGS); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java index e30375e80945..e0011096e464 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java @@ -97,7 +97,7 @@ public Result createProjectParameter(User loginUser, long projectCode, String pr .builder() .paramName(projectParameterName) .paramValue(projectParameterValue) - .code(CodeGenerateUtils.getInstance().genCode()) + .code(CodeGenerateUtils.genCode()) .projectCode(projectCode) .userId(loginUser.getId()) .createTime(now) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectPreferenceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectPreferenceServiceImpl.java index dcbd3b745602..6274d290d5a1 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectPreferenceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectPreferenceServiceImpl.java @@ -76,7 +76,7 @@ public Result updateProjectPreference(User loginUser, long projectCode, String p projectPreference.setProjectCode(projectCode); projectPreference.setPreferences(preferences); projectPreference.setUserId(loginUser.getId()); - projectPreference.setCode(CodeGenerateUtils.getInstance().genCode()); + projectPreference.setCode(CodeGenerateUtils.genCode()); projectPreference.setState(1); projectPreference.setCreateTime(now); projectPreference.setUpdateTime(now); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectServiceImpl.java index 1a00584a26f5..b5c329c4fd0e 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectServiceImpl.java @@ -126,7 +126,7 @@ public Result createProject(User loginUser, String name, String desc) { project = Project .builder() .name(name) - .code(CodeGenerateUtils.getInstance().genCode()) + .code(CodeGenerateUtils.genCode()) .description(desc) .userId(loginUser.getId()) .userName(loginUser.getUserName()) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java index 4a0c3f8b68d7..8b01df13194e 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java @@ -265,7 +265,7 @@ public TaskDefinition createTaskDefinitionV2(User loginUser, long taskDefinitionCode; try { - taskDefinitionCode = CodeGenerateUtils.getInstance().genCode(); + taskDefinitionCode = CodeGenerateUtils.genCode(); } catch (CodeGenerateException e) { throw new ServiceException(Status.INTERNAL_SERVER_ERROR_ARGS); } @@ -338,7 +338,7 @@ public Map createTaskBindsWorkFlow(User loginUser, } long taskCode = taskDefinition.getCode(); if (taskCode == 0) { - taskDefinition.setCode(CodeGenerateUtils.getInstance().genCode()); + taskDefinition.setCode(CodeGenerateUtils.genCode()); } List processTaskRelationLogList = processTaskRelationMapper.queryByProcessCode(processDefinitionCode) @@ -1264,7 +1264,7 @@ public Map genTaskCodeList(Integer genNum) { List taskCodes = new ArrayList<>(); try { for (int i = 0; i < genNum; i++) { - taskCodes.add(CodeGenerateUtils.getInstance().genCode()); + taskCodes.add(CodeGenerateUtils.genCode()); } } catch (CodeGenerateException e) { log.error("Generate task definition code error.", e); diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceTest.java index b226295e0e4e..b8161af13dfd 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/impl/EnvironmentServiceTest.java @@ -96,9 +96,6 @@ public class EnvironmentServiceTest { @Mock private ResourcePermissionCheckService resourcePermissionCheckService; - @Mock - private CodeGenerateUtils codeGenerateUtils; - public static final String testUserName = "environmentServerTest"; public static final String environmentName = "Env1"; @@ -141,8 +138,7 @@ public void testCreateEnvironment() { () -> environmentService.createEnvironment(adminUser, "testName", "test", "test", workerGroups)); try (MockedStatic ignored = Mockito.mockStatic(CodeGenerateUtils.class)) { - when(CodeGenerateUtils.getInstance()).thenReturn(codeGenerateUtils); - when(codeGenerateUtils.genCode()).thenThrow(CodeGenerateUtils.CodeGenerateException.class); + when(CodeGenerateUtils.genCode()).thenThrow(CodeGenerateUtils.CodeGenerateException.class); assertThrowsServiceException(Status.INTERNAL_SERVER_ERROR_ARGS, () -> environmentService.createEnvironment(adminUser, "testName", "test", "test", workerGroups)); diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/CodeGenerateUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/CodeGenerateUtils.java index f35523b59df7..3e75264808ae 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/CodeGenerateUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/CodeGenerateUtils.java @@ -1,4 +1,6 @@ -/** Copyright 2010-2012 Twitter, Inc.*/ +/** + * Copyright 2010-2012 Twitter, Inc. + */ package org.apache.dolphinscheduler.common.utils; @@ -6,66 +8,71 @@ import java.net.UnknownHostException; import java.util.Objects; +import lombok.extern.slf4j.Slf4j; + /** - * Rewriting based on Twitter snowflake algorithm + * Rewriting based on Twitter snowflake algorithm */ +@Slf4j public class CodeGenerateUtils { - // start timestamp - private static final long START_TIMESTAMP = 1609430400000L; // 2021-01-01 00:00:00 - // Each machine generates 32 in the same millisecond - private static final long LOW_DIGIT_BIT = 5L; - private static final long MIDDLE_BIT = 2L; - private static final long MAX_LOW_DIGIT = ~(-1L << LOW_DIGIT_BIT); - // The displacement to the left - private static final long MIDDLE_LEFT = LOW_DIGIT_BIT; - private static final long HIGH_DIGIT_LEFT = LOW_DIGIT_BIT + MIDDLE_BIT; - private final long machineHash; - private long lowDigit = 0L; - private long recordMillisecond = -1L; - - private static final long SYSTEM_TIMESTAMP = System.currentTimeMillis(); - private static final long SYSTEM_NANOTIME = System.nanoTime(); + private static final CodeGenerator codeGenerator; - private CodeGenerateUtils() throws CodeGenerateException { + static { try { - this.machineHash = - Math.abs(Objects.hash(InetAddress.getLocalHost().getHostName())) % (2 << (MIDDLE_BIT - 1)); + codeGenerator = new CodeGenerator(InetAddress.getLocalHost().getHostName() + "-" + OSUtils.getProcessID()); } catch (UnknownHostException e) { throw new CodeGenerateException(e.getMessage()); } } - private static CodeGenerateUtils instance = null; - - public static synchronized CodeGenerateUtils getInstance() throws CodeGenerateException { - if (instance == null) { - instance = new CodeGenerateUtils(); - } - return instance; + public static long genCode() throws CodeGenerateException { + return codeGenerator.genCode(); } - public synchronized long genCode() throws CodeGenerateException { - long nowtMillisecond = systemMillisecond(); - if (nowtMillisecond < recordMillisecond) { - throw new CodeGenerateException("New code exception because time is set back."); + public static class CodeGenerator { + + // start timestamp + private static final long START_TIMESTAMP = 1609430400000L; // 2021-01-01 00:00:00 + // Each machine generates 32 in the same millisecond + private static final long LOW_DIGIT_BIT = 5L; + private static final long MACHINE_BIT = 5L; + private static final long MAX_LOW_DIGIT = ~(-1L << LOW_DIGIT_BIT); + // The displacement to the left + private static final long HIGH_DIGIT_LEFT = LOW_DIGIT_BIT + MACHINE_BIT; + public final long machineHash; + private long lowDigit = 0L; + private long recordMillisecond = -1L; + + private static final long SYSTEM_TIMESTAMP = System.currentTimeMillis(); + private static final long SYSTEM_NANOTIME = System.nanoTime(); + + public CodeGenerator(String appName) { + this.machineHash = Math.abs(Objects.hash(appName)) % (1 << MACHINE_BIT); } - if (nowtMillisecond == recordMillisecond) { - lowDigit = (lowDigit + 1) & MAX_LOW_DIGIT; - if (lowDigit == 0L) { - while (nowtMillisecond <= recordMillisecond) { - nowtMillisecond = systemMillisecond(); + + public synchronized long genCode() throws CodeGenerateException { + long nowtMillisecond = systemMillisecond(); + if (nowtMillisecond < recordMillisecond) { + throw new CodeGenerateException("New code exception because time is set back."); + } + if (nowtMillisecond == recordMillisecond) { + lowDigit = (lowDigit + 1) & MAX_LOW_DIGIT; + if (lowDigit == 0L) { + while (nowtMillisecond <= recordMillisecond) { + nowtMillisecond = systemMillisecond(); + } } + } else { + lowDigit = 0L; } - } else { - lowDigit = 0L; + recordMillisecond = nowtMillisecond; + return (nowtMillisecond - START_TIMESTAMP) << HIGH_DIGIT_LEFT | machineHash << LOW_DIGIT_BIT | lowDigit; } - recordMillisecond = nowtMillisecond; - return (nowtMillisecond - START_TIMESTAMP) << HIGH_DIGIT_LEFT | machineHash << MIDDLE_LEFT | lowDigit; - } - private long systemMillisecond() { - return SYSTEM_TIMESTAMP + (System.nanoTime() - SYSTEM_NANOTIME) / 1000000; + private long systemMillisecond() { + return SYSTEM_TIMESTAMP + (System.nanoTime() - SYSTEM_NANOTIME) / 1000000; + } } public static class CodeGenerateException extends RuntimeException { diff --git a/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/CodeGenerateUtilsTest.java b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/CodeGenerateUtilsTest.java index 3871646c95c8..8cd8ab8e6d4f 100644 --- a/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/CodeGenerateUtilsTest.java +++ b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/CodeGenerateUtilsTest.java @@ -17,20 +17,59 @@ package org.apache.dolphinscheduler.common.utils; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class CodeGenerateUtilsTest { +class CodeGenerateUtilsTest { @Test - public void testNoGenerateDuplicateCode() throws CodeGenerateUtils.CodeGenerateException { - HashSet existsCode = new HashSet<>(); - for (int i = 0; i < 100; i++) { - Long currentCode = CodeGenerateUtils.getInstance().genCode(); - Assertions.assertFalse(existsCode.contains(currentCode)); + void testNoGenerateDuplicateCode() { + int codeNum = 10000000; + List existsCode = new ArrayList<>(); + for (int i = 0; i < codeNum; i++) { + Long currentCode = CodeGenerateUtils.genCode(); existsCode.add(currentCode); } + Set existsCodeSet = new HashSet<>(existsCode); + // Disallow duplicate code + assertEquals(existsCode.size(), existsCodeSet.size()); + } + + @Test + void testNoGenerateDuplicateCodeWithDifferentAppName() throws UnknownHostException, InterruptedException { + int threadNum = 10; + int codeNum = 1000000; + + final String hostName = InetAddress.getLocalHost().getHostName(); + Map> machineCodes = new ConcurrentHashMap<>(); + CountDownLatch countDownLatch = new CountDownLatch(threadNum); + + for (int i = 0; i < threadNum; i++) { + final int c = i; + new Thread(() -> { + List codes = new ArrayList<>(codeNum); + CodeGenerateUtils.CodeGenerator codeGenerator = new CodeGenerateUtils.CodeGenerator(hostName + "-" + c); + for (int j = 0; j < codeNum; j++) { + codes.add(codeGenerator.genCode()); + } + machineCodes.put(Thread.currentThread().getName(), codes); + countDownLatch.countDown(); + }).start(); + } + countDownLatch.await(); + Set totalCodes = new HashSet<>(); + machineCodes.values().forEach(totalCodes::addAll); + assertEquals(codeNum * threadNum, totalCodes.size()); } } diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java index 40191f6aa3b5..028ab7651f11 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java @@ -1679,7 +1679,7 @@ public int saveTaskDefine(User operator, long projectCode, List queryProcessDefinition(Connection conn) { processDefinition.setId(rs.getInt(1)); long code = rs.getLong(2); if (code == 0L) { - code = CodeGenerateUtils.getInstance().genCode(); + code = CodeGenerateUtils.genCode(); } processDefinition.setCode(code); processDefinition.setVersion(Constants.VERSION_FIRST); diff --git a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/dao/ProjectDao.java b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/dao/ProjectDao.java index 65466fe99a3c..685732337ae4 100644 --- a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/dao/ProjectDao.java +++ b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/dao/ProjectDao.java @@ -47,7 +47,7 @@ public Map queryAllProject(Connection conn) { Integer id = rs.getInt(1); long code = rs.getLong(2); if (code == 0L) { - code = CodeGenerateUtils.getInstance().genCode(); + code = CodeGenerateUtils.genCode(); } projectMap.put(id, code); } diff --git a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/upgrader/v200/V200DolphinSchedulerUpgrader.java b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/upgrader/v200/V200DolphinSchedulerUpgrader.java index 35a9be75dc43..fd430c2b0622 100644 --- a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/upgrader/v200/V200DolphinSchedulerUpgrader.java +++ b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/upgrader/v200/V200DolphinSchedulerUpgrader.java @@ -206,7 +206,7 @@ private void splitProcessDefinitionJson(List processDefinitio taskDefinitionLog.setName(name); taskDefinitionLog .setWorkerGroup(task.get("workerGroup") == null ? "default" : task.get("workerGroup").asText()); - long taskCode = CodeGenerateUtils.getInstance().genCode(); + long taskCode = CodeGenerateUtils.genCode(); taskDefinitionLog.setCode(taskCode); taskDefinitionLog.setVersion(Constants.VERSION_FIRST); taskDefinitionLog.setProjectCode(processDefinition.getProjectCode()); diff --git a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/demo/ProcessDefinitionDemo.java b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/demo/ProcessDefinitionDemo.java index fb1cfab63790..023928575f3f 100644 --- a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/demo/ProcessDefinitionDemo.java +++ b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/demo/ProcessDefinitionDemo.java @@ -85,7 +85,7 @@ public void createProcessDefinitionDemo() throws Exception { project = Project .builder() .name("demo") - .code(CodeGenerateUtils.getInstance().genCode()) + .code(CodeGenerateUtils.genCode()) .description("") .userId(loginUser.getId()) .userName(loginUser.getUserName()) @@ -167,7 +167,7 @@ public ProxyResult clearLogDemo(String token, long projectCode, String tenantCod List taskCodes = new ArrayList<>(); try { for (int i = 0; i < 1; i++) { - taskCodes.add(CodeGenerateUtils.getInstance().genCode()); + taskCodes.add(CodeGenerateUtils.genCode()); } } catch (CodeGenerateUtils.CodeGenerateException e) { log.error("task code get error, ", e); @@ -242,7 +242,7 @@ public ProxyResult dependentProxyResultDemo(String token, long projectCode, Stri List taskCodes = new ArrayList<>(); try { for (int i = 0; i < 2; i++) { - taskCodes.add(CodeGenerateUtils.getInstance().genCode()); + taskCodes.add(CodeGenerateUtils.genCode()); } } catch (CodeGenerateUtils.CodeGenerateException e) { log.error("task code get error, ", e); @@ -334,7 +334,7 @@ public ProxyResult parameterContextDemo(String token, long projectCode, String t List taskCodes = new ArrayList<>(); try { for (int i = 0; i < 2; i++) { - taskCodes.add(CodeGenerateUtils.getInstance().genCode()); + taskCodes.add(CodeGenerateUtils.genCode()); } } catch (CodeGenerateUtils.CodeGenerateException e) { log.error("task code get error, ", e); @@ -420,7 +420,7 @@ public ProxyResult conditionDemo(String token, long projectCode, String tenantCo List taskCodes = new ArrayList<>(); try { for (int i = 0; i < 4; i++) { - taskCodes.add(CodeGenerateUtils.getInstance().genCode()); + taskCodes.add(CodeGenerateUtils.genCode()); } } catch (CodeGenerateUtils.CodeGenerateException e) { log.error("task code get error, ", e); @@ -537,7 +537,7 @@ public ProxyResult swicthDemo(String token, long projectCode, String tenantCode) List taskCodes = new ArrayList<>(); try { for (int i = 0; i < 4; i++) { - taskCodes.add(CodeGenerateUtils.getInstance().genCode()); + taskCodes.add(CodeGenerateUtils.genCode()); } } catch (CodeGenerateUtils.CodeGenerateException e) { log.error("task code get error, ", e); @@ -656,7 +656,7 @@ public ProxyResult shellDemo(String token, long projectCode, String tenantCode) List taskCodes = new ArrayList<>(); try { for (int i = 0; i < 3; i++) { - taskCodes.add(CodeGenerateUtils.getInstance().genCode()); + taskCodes.add(CodeGenerateUtils.genCode()); } } catch (CodeGenerateUtils.CodeGenerateException e) { log.error("task code get error, ", e); @@ -755,7 +755,7 @@ public ProxyResult subProcessDemo(String token, long projectCode, String tenantC List taskCodes = new ArrayList<>(); try { for (int i = 0; i < 1; i++) { - taskCodes.add(CodeGenerateUtils.getInstance().genCode()); + taskCodes.add(CodeGenerateUtils.genCode()); } } catch (CodeGenerateUtils.CodeGenerateException e) { log.error("task code get error, ", e); From 76d059810a928c3a122df3ee917cb5eedaf60063 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Tue, 16 Apr 2024 16:19:57 +0800 Subject: [PATCH 031/165] Forbidden forcess success a task instance in a running workflow instance (#15855) --- .../controller/TaskInstanceController.java | 9 +- .../v2/TaskInstanceV2Controller.java | 4 +- .../api/service/TaskInstanceService.java | 6 +- .../service/impl/TaskInstanceServiceImpl.java | 62 ++++----- .../api/AssertionsHelper.java | 8 ++ .../TaskInstanceControllerTest.java | 4 +- .../v2/TaskInstanceV2ControllerTest.java | 8 +- .../api/service/TaskInstanceServiceTest.java | 119 +++++++++--------- .../service/process/ProcessService.java | 2 +- .../service/process/ProcessServiceImpl.java | 13 +- 10 files changed, 115 insertions(+), 120 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/TaskInstanceController.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/TaskInstanceController.java index 1865dfee5d4e..e0055595c9b1 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/TaskInstanceController.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/TaskInstanceController.java @@ -153,10 +153,11 @@ public Result queryTaskListPaging(@Parameter(hidden = true) @RequestAttribute(va @ResponseStatus(HttpStatus.OK) @ApiException(FORCE_TASK_SUCCESS_ERROR) @OperatorLog(auditType = AuditType.TASK_INSTANCE_FORCE_SUCCESS) - public Result forceTaskSuccess(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, - @Schema(name = "projectCode", required = true) @PathVariable long projectCode, - @PathVariable(value = "id") Integer id) { - return taskInstanceService.forceTaskSuccess(loginUser, projectCode, id); + public Result forceTaskSuccess(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, + @Schema(name = "projectCode", required = true) @PathVariable long projectCode, + @PathVariable(value = "id") Integer id) { + taskInstanceService.forceTaskSuccess(loginUser, projectCode, id); + return Result.success(); } /** diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/v2/TaskInstanceV2Controller.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/v2/TaskInstanceV2Controller.java index 3e3a87681b69..ea767f0fa33a 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/v2/TaskInstanceV2Controller.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/v2/TaskInstanceV2Controller.java @@ -167,8 +167,8 @@ public Result stopTask(@Parameter(hidden = true) @RequestAttribute(value public TaskInstanceSuccessResponse forceTaskSuccess(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, @Parameter(name = "projectCode", description = "PROJECT_CODE", required = true) @PathVariable long projectCode, @PathVariable(value = "id") Integer id) { - Result result = taskInstanceService.forceTaskSuccess(loginUser, projectCode, id); - return new TaskInstanceSuccessResponse(result); + taskInstanceService.forceTaskSuccess(loginUser, projectCode, id); + return new TaskInstanceSuccessResponse(Result.success()); } /** diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/TaskInstanceService.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/TaskInstanceService.java index 86e5396dbe48..bff051868578 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/TaskInstanceService.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/TaskInstanceService.java @@ -72,9 +72,9 @@ Result queryTaskListPaging(User loginUser, * @param taskInstanceId task instance id * @return the result code and msg */ - Result forceTaskSuccess(User loginUser, - long projectCode, - Integer taskInstanceId); + void forceTaskSuccess(User loginUser, + long projectCode, + Integer taskInstanceId); /** * task savepoint diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskInstanceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskInstanceServiceImpl.java index f06f8115a924..7469d8db1323 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskInstanceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskInstanceServiceImpl.java @@ -23,6 +23,7 @@ import org.apache.dolphinscheduler.api.dto.taskInstance.TaskInstanceRemoveCacheResponse; import org.apache.dolphinscheduler.api.enums.Status; +import org.apache.dolphinscheduler.api.exceptions.ServiceException; import org.apache.dolphinscheduler.api.service.ProjectService; import org.apache.dolphinscheduler.api.service.TaskGroupQueueService; import org.apache.dolphinscheduler.api.service.TaskInstanceService; @@ -33,14 +34,15 @@ import org.apache.dolphinscheduler.common.enums.TaskExecuteType; import org.apache.dolphinscheduler.common.utils.CollectionUtils; import org.apache.dolphinscheduler.common.utils.DateUtils; +import org.apache.dolphinscheduler.dao.entity.ProcessInstance; import org.apache.dolphinscheduler.dao.entity.Project; -import org.apache.dolphinscheduler.dao.entity.TaskDefinition; import org.apache.dolphinscheduler.dao.entity.TaskInstance; import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.dao.mapper.ProjectMapper; import org.apache.dolphinscheduler.dao.mapper.TaskDefinitionMapper; import org.apache.dolphinscheduler.dao.mapper.TaskInstanceMapper; import org.apache.dolphinscheduler.dao.repository.DqExecuteResultDao; +import org.apache.dolphinscheduler.dao.repository.ProcessInstanceDao; import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao; import org.apache.dolphinscheduler.dao.utils.TaskCacheUtils; import org.apache.dolphinscheduler.extract.base.client.SingletonJdkDynamicRpcClientProxyFactory; @@ -107,6 +109,9 @@ public class TaskInstanceServiceImpl extends BaseServiceImpl implements TaskInst @Autowired private TaskGroupQueueService taskGroupQueueService; + @Autowired + private ProcessInstanceDao workflowInstanceDao; + /** * query task list by project, process instance, task name, task start time, task end time, task status, keyword paging * @@ -216,58 +221,39 @@ public Result queryTaskListPaging(User loginUser, */ @Transactional @Override - public Result forceTaskSuccess(User loginUser, long projectCode, Integer taskInstanceId) { - Result result = new Result(); - Project project = projectMapper.queryByCode(projectCode); + public void forceTaskSuccess(User loginUser, long projectCode, Integer taskInstanceId) { // check user access for project - Map checkResult = - projectService.checkProjectAndAuth(loginUser, project, projectCode, FORCED_SUCCESS); - Status status = (Status) checkResult.get(Constants.STATUS); - if (status != Status.SUCCESS) { - putMsg(result, status); - return result; - } + projectService.checkProjectAndAuthThrowException(loginUser, projectCode, FORCED_SUCCESS); - // check whether the task instance can be found - TaskInstance task = taskInstanceMapper.selectById(taskInstanceId); - if (task == null) { - log.error("Task instance can not be found, projectCode:{}, taskInstanceId:{}.", projectCode, - taskInstanceId); - putMsg(result, Status.TASK_INSTANCE_NOT_FOUND); - return result; + TaskInstance task = taskInstanceDao.queryOptionalById(taskInstanceId) + .orElseThrow(() -> new ServiceException(Status.TASK_INSTANCE_NOT_FOUND)); + + if (task.getProjectCode() != projectCode) { + throw new ServiceException("The task instance is not under the project: " + projectCode); } - TaskDefinition taskDefinition = taskDefinitionMapper.queryByCode(task.getTaskCode()); - if (taskDefinition != null && projectCode != taskDefinition.getProjectCode()) { - log.error("Task definition can not be found, projectCode:{}, taskDefinitionCode:{}.", projectCode, - task.getTaskCode()); - putMsg(result, Status.TASK_INSTANCE_NOT_FOUND, taskInstanceId); - return result; + ProcessInstance processInstance = workflowInstanceDao.queryOptionalById(task.getProcessInstanceId()) + .orElseThrow( + () -> new ServiceException(Status.PROCESS_INSTANCE_NOT_EXIST, task.getProcessInstanceId())); + if (!processInstance.getState().isFinished()) { + throw new ServiceException("The workflow instance is not finished: " + processInstance.getState() + + " cannot force start task instance"); } // check whether the task instance state type is failure or cancel if (!task.getState().isFailure() && !task.getState().isKill()) { - log.warn("{} type task instance can not perform force success, projectCode:{}, taskInstanceId:{}.", - task.getState().getDesc(), projectCode, taskInstanceId); - putMsg(result, Status.TASK_INSTANCE_STATE_OPERATION_ERROR, taskInstanceId, task.getState().toString()); - return result; + throw new ServiceException(Status.TASK_INSTANCE_STATE_OPERATION_ERROR, taskInstanceId, task.getState()); } // change the state of the task instance task.setState(TaskExecutionStatus.FORCED_SUCCESS); task.setEndTime(new Date()); int changedNum = taskInstanceMapper.updateById(task); - if (changedNum > 0) { - processService.forceProcessInstanceSuccessByTaskInstanceId(taskInstanceId); - log.info("Task instance performs force success complete, projectCode:{}, taskInstanceId:{}", projectCode, - taskInstanceId); - putMsg(result, Status.SUCCESS); - } else { - log.error("Task instance performs force success complete, projectCode:{}, taskInstanceId:{}", - projectCode, taskInstanceId); - putMsg(result, Status.FORCE_TASK_SUCCESS_ERROR); + if (changedNum <= 0) { + throw new ServiceException(Status.FORCE_TASK_SUCCESS_ERROR); } - return result; + processService.forceProcessInstanceSuccessByTaskInstanceId(task); + log.info("Force success task instance:{} success", taskInstanceId); } @Override diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/AssertionsHelper.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/AssertionsHelper.java index d2da5bc638c0..eae064bb24d3 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/AssertionsHelper.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/AssertionsHelper.java @@ -20,6 +20,8 @@ import org.apache.dolphinscheduler.api.enums.Status; import org.apache.dolphinscheduler.api.exceptions.ServiceException; +import java.text.MessageFormat; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.function.Executable; @@ -30,6 +32,12 @@ public static void assertThrowsServiceException(Status status, Executable execut Assertions.assertEquals(status.getCode(), exception.getCode()); } + public static void assertThrowsServiceException(String message, Executable executable) { + ServiceException exception = Assertions.assertThrows(ServiceException.class, executable); + Assertions.assertEquals(MessageFormat.format(Status.INTERNAL_SERVER_ERROR_ARGS.getMsg(), message), + exception.getMessage()); + } + public static void assertDoesNotThrow(Executable executable) { Assertions.assertDoesNotThrow(executable); } diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/TaskInstanceControllerTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/TaskInstanceControllerTest.java index 7ebe5bf7576d..b58944537b87 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/TaskInstanceControllerTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/TaskInstanceControllerTest.java @@ -82,9 +82,7 @@ public void testForceTaskSuccess() throws Exception { MultiValueMap paramsMap = new LinkedMultiValueMap<>(); paramsMap.add("taskInstanceId", "104"); - Result mockResult = new Result(); - putMsg(mockResult, Status.SUCCESS); - when(taskInstanceService.forceTaskSuccess(any(User.class), anyLong(), anyInt())).thenReturn(mockResult); + Mockito.doNothing().when(taskInstanceService).forceTaskSuccess(any(User.class), anyLong(), anyInt()); MvcResult mvcResult = mockMvc.perform(post("/projects/{projectName}/task-instance/force-success", "cxc_1113") .header(SESSION_ID, sessionId) diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/v2/TaskInstanceV2ControllerTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/v2/TaskInstanceV2ControllerTest.java index cc70f200e1eb..8e76ec1694f6 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/v2/TaskInstanceV2ControllerTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/v2/TaskInstanceV2ControllerTest.java @@ -23,6 +23,7 @@ import org.apache.dolphinscheduler.api.controller.AbstractControllerTest; import org.apache.dolphinscheduler.api.dto.taskInstance.TaskInstanceQueryRequest; +import org.apache.dolphinscheduler.api.dto.taskInstance.TaskInstanceSuccessResponse; import org.apache.dolphinscheduler.api.enums.Status; import org.apache.dolphinscheduler.api.service.TaskInstanceService; import org.apache.dolphinscheduler.api.utils.PageInfo; @@ -85,12 +86,9 @@ public void testQueryTaskListPaging() { @Test public void testForceTaskSuccess() { - Result mockResult = new Result(); - putMsg(mockResult, Status.SUCCESS); - - when(taskInstanceService.forceTaskSuccess(any(), Mockito.anyLong(), Mockito.anyInt())).thenReturn(mockResult); + Mockito.doNothing().when(taskInstanceService).forceTaskSuccess(any(), Mockito.anyLong(), Mockito.anyInt()); - Result taskResult = taskInstanceV2Controller.forceTaskSuccess(null, 1L, 1); + TaskInstanceSuccessResponse taskResult = taskInstanceV2Controller.forceTaskSuccess(null, 1L, 1); Assertions.assertEquals(Integer.valueOf(Status.SUCCESS.getCode()), taskResult.getCode()); } diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/TaskInstanceServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/TaskInstanceServiceTest.java index aca0d80a6fae..dd7acb16d577 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/TaskInstanceServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/TaskInstanceServiceTest.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.api.service; +import static org.apache.dolphinscheduler.api.AssertionsHelper.assertThrowsServiceException; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.FORCED_SUCCESS; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.TASK_INSTANCE; import static org.mockito.ArgumentMatchers.any; @@ -25,7 +26,6 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; -import org.apache.dolphinscheduler.api.ApiApplicationServer; import org.apache.dolphinscheduler.api.dto.taskInstance.TaskInstanceRemoveCacheResponse; import org.apache.dolphinscheduler.api.enums.Status; import org.apache.dolphinscheduler.api.exceptions.ServiceException; @@ -35,15 +35,16 @@ import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.enums.TaskExecuteType; import org.apache.dolphinscheduler.common.enums.UserType; +import org.apache.dolphinscheduler.common.enums.WorkflowExecutionStatus; import org.apache.dolphinscheduler.common.utils.DateUtils; import org.apache.dolphinscheduler.dao.entity.ProcessInstance; import org.apache.dolphinscheduler.dao.entity.Project; -import org.apache.dolphinscheduler.dao.entity.TaskDefinition; import org.apache.dolphinscheduler.dao.entity.TaskInstance; import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.dao.mapper.ProjectMapper; import org.apache.dolphinscheduler.dao.mapper.TaskDefinitionMapper; import org.apache.dolphinscheduler.dao.mapper.TaskInstanceMapper; +import org.apache.dolphinscheduler.dao.repository.ProcessInstanceDao; import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao; import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus; import org.apache.dolphinscheduler.service.process.ProcessService; @@ -65,7 +66,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.boot.test.context.SpringBootTest; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -74,7 +74,6 @@ */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -@SpringBootTest(classes = ApiApplicationServer.class) public class TaskInstanceServiceTest { @InjectMocks @@ -100,6 +99,8 @@ public class TaskInstanceServiceTest { @Mock TaskInstanceDao taskInstanceDao; + @Mock + ProcessInstanceDao workflowInstanceDao; @Test public void queryTaskListPaging() { @@ -324,6 +325,7 @@ private ProcessInstance getProcessInstance() { private TaskInstance getTaskInstance() { TaskInstance taskInstance = new TaskInstance(); taskInstance.setId(1); + taskInstance.setProjectCode(1L); taskInstance.setName("test_task_instance"); taskInstance.setStartTime(new Date()); taskInstance.setEndTime(new Date()); @@ -343,64 +345,69 @@ private void putMsg(Map result, Status status, Object... statusP } @Test - public void testForceTaskSuccess() { + public void testForceTaskSuccess_withNoPermission() { + User user = getAdminUser(); + TaskInstance task = getTaskInstance(); + doThrow(new ServiceException(Status.USER_NO_OPERATION_PROJECT_PERM)).when(projectService) + .checkProjectAndAuthThrowException(user, task.getProjectCode(), FORCED_SUCCESS); + assertThrowsServiceException(Status.USER_NO_OPERATION_PROJECT_PERM, + () -> taskInstanceService.forceTaskSuccess(user, task.getProjectCode(), task.getId())); + } + + @Test + public void testForceTaskSuccess_withTaskInstanceNotFound() { + User user = getAdminUser(); + TaskInstance task = getTaskInstance(); + doNothing().when(projectService).checkProjectAndAuthThrowException(user, task.getProjectCode(), FORCED_SUCCESS); + when(taskInstanceDao.queryOptionalById(task.getId())).thenReturn(Optional.empty()); + assertThrowsServiceException(Status.TASK_INSTANCE_NOT_FOUND, + () -> taskInstanceService.forceTaskSuccess(user, task.getProjectCode(), task.getId())); + } + + @Test + public void testForceTaskSuccess_withWorkflowInstanceNotFound() { + User user = getAdminUser(); + TaskInstance task = getTaskInstance(); + doNothing().when(projectService).checkProjectAndAuthThrowException(user, task.getProjectCode(), FORCED_SUCCESS); + when(taskInstanceDao.queryOptionalById(task.getId())).thenReturn(Optional.of(task)); + when(workflowInstanceDao.queryOptionalById(task.getProcessInstanceId())).thenReturn(Optional.empty()); + + assertThrowsServiceException(Status.PROCESS_INSTANCE_NOT_EXIST, + () -> taskInstanceService.forceTaskSuccess(user, task.getProjectCode(), task.getId())); + } + + @Test + public void testForceTaskSuccess_withWorkflowInstanceNotFinished() { User user = getAdminUser(); long projectCode = 1L; - Project project = getProject(projectCode); - int taskId = 1; TaskInstance task = getTaskInstance(); + ProcessInstance processInstance = getProcessInstance(); + processInstance.setState(WorkflowExecutionStatus.RUNNING_EXECUTION); + doNothing().when(projectService).checkProjectAndAuthThrowException(user, projectCode, FORCED_SUCCESS); + when(taskInstanceDao.queryOptionalById(task.getId())).thenReturn(Optional.of(task)); + when(workflowInstanceDao.queryOptionalById(task.getProcessInstanceId())) + .thenReturn(Optional.of(processInstance)); - Map mockSuccess = new HashMap<>(5); - putMsg(mockSuccess, Status.SUCCESS); - when(projectMapper.queryByCode(projectCode)).thenReturn(project); + assertThrowsServiceException( + "The workflow instance is not finished: " + processInstance.getState() + + " cannot force start task instance", + () -> taskInstanceService.forceTaskSuccess(user, projectCode, task.getId())); + } - // user auth failed - Map mockFailure = new HashMap<>(5); - putMsg(mockFailure, Status.USER_NO_OPERATION_PROJECT_PERM, user.getUserName(), projectCode); - when(projectService.checkProjectAndAuth(user, project, projectCode, FORCED_SUCCESS)).thenReturn(mockFailure); - Result authFailRes = taskInstanceService.forceTaskSuccess(user, projectCode, taskId); - Assertions.assertNotSame(Status.SUCCESS.getCode(), authFailRes.getCode()); - - // test task not found - when(projectService.checkProjectAndAuth(user, project, projectCode, FORCED_SUCCESS)).thenReturn(mockSuccess); - when(taskInstanceMapper.selectById(Mockito.anyInt())).thenReturn(null); - TaskDefinition taskDefinition = new TaskDefinition(); - taskDefinition.setProjectCode(projectCode); - when(taskDefinitionMapper.queryByCode(task.getTaskCode())).thenReturn(taskDefinition); - Result taskNotFoundRes = taskInstanceService.forceTaskSuccess(user, projectCode, taskId); - Assertions.assertEquals(Status.TASK_INSTANCE_NOT_FOUND.getCode(), taskNotFoundRes.getCode().intValue()); - - // test task instance state error - task.setState(TaskExecutionStatus.SUCCESS); - when(taskInstanceMapper.selectById(1)).thenReturn(task); - Map result = new HashMap<>(); - putMsg(result, Status.SUCCESS, projectCode); - when(projectMapper.queryByCode(projectCode)).thenReturn(project); - when(projectService.checkProjectAndAuth(user, project, projectCode, FORCED_SUCCESS)).thenReturn(result); - Result taskStateErrorRes = taskInstanceService.forceTaskSuccess(user, projectCode, taskId); - Assertions.assertEquals(Status.TASK_INSTANCE_STATE_OPERATION_ERROR.getCode(), - taskStateErrorRes.getCode().intValue()); - - // test error - task.setState(TaskExecutionStatus.FAILURE); - when(taskInstanceMapper.updateById(task)).thenReturn(0); - putMsg(result, Status.SUCCESS, projectCode); - when(projectMapper.queryByCode(projectCode)).thenReturn(project); - when(projectService.checkProjectAndAuth(user, project, projectCode, FORCED_SUCCESS)).thenReturn(result); - Result errorRes = taskInstanceService.forceTaskSuccess(user, projectCode, taskId); - Assertions.assertEquals(Status.FORCE_TASK_SUCCESS_ERROR.getCode(), errorRes.getCode().intValue()); - - // test success - task.setState(TaskExecutionStatus.FAILURE); - task.setEndTime(null); - when(taskInstanceMapper.updateById(task)).thenReturn(1); - putMsg(result, Status.SUCCESS, projectCode); - when(projectMapper.queryByCode(projectCode)).thenReturn(project); - when(projectService.checkProjectAndAuth(user, project, projectCode, FORCED_SUCCESS)).thenReturn(result); - Result successRes = taskInstanceService.forceTaskSuccess(user, projectCode, taskId); - Assertions.assertEquals(Status.SUCCESS.getCode(), successRes.getCode().intValue()); - Assertions.assertNotNull(task.getEndTime()); + @Test + public void testForceTaskSuccess_withTaskInstanceNotFinished() { + User user = getAdminUser(); + TaskInstance task = getTaskInstance(); + ProcessInstance processInstance = getProcessInstance(); + processInstance.setState(WorkflowExecutionStatus.FAILURE); + doNothing().when(projectService).checkProjectAndAuthThrowException(user, task.getProjectCode(), FORCED_SUCCESS); + when(taskInstanceDao.queryOptionalById(task.getId())).thenReturn(Optional.of(task)); + when(workflowInstanceDao.queryOptionalById(task.getProcessInstanceId())) + .thenReturn(Optional.of(processInstance)); + assertThrowsServiceException( + Status.TASK_INSTANCE_STATE_OPERATION_ERROR, + () -> taskInstanceService.forceTaskSuccess(user, task.getProjectCode(), task.getId())); } @Test diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessService.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessService.java index 101350e90dd7..8787aabc8bbb 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessService.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessService.java @@ -189,7 +189,7 @@ TaskGroupQueue insertIntoTaskGroupQueue(Integer taskId, public String findConfigYamlByName(String clusterName); - void forceProcessInstanceSuccessByTaskInstanceId(Integer taskInstanceId); + void forceProcessInstanceSuccessByTaskInstanceId(TaskInstance taskInstance); void saveCommandTrigger(Integer commandId, Integer processInstanceId); diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java index 028ab7651f11..3c207ae9829a 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java @@ -276,6 +276,7 @@ public class ProcessServiceImpl implements ProcessService { @Autowired private TriggerRelationService triggerRelationService; + /** * todo: split this method * handle Command (construct ProcessInstance from Command) , wrapped in transaction @@ -621,13 +622,13 @@ public void setGlobalParamIfCommanded(ProcessDefinition processDefinition, Map * the workflow provides a tenant and uses the provided tenant; * when no tenant is provided or the provided tenant is the default tenant, \ * the user's tenant created by the workflow is used * * @param tenantCode tenantCode - * @param userId userId + * @param userId userId * @return tenant code */ @Override @@ -2114,11 +2115,7 @@ public String findConfigYamlByName(String clusterName) { } @Override - public void forceProcessInstanceSuccessByTaskInstanceId(Integer taskInstanceId) { - TaskInstance task = taskInstanceMapper.selectById(taskInstanceId); - if (task == null) { - return; - } + public void forceProcessInstanceSuccessByTaskInstanceId(TaskInstance task) { ProcessInstance processInstance = findProcessInstanceDetailById(task.getProcessInstanceId()).orElse(null); if (processInstance != null && (processInstance.getState().isFailure() || processInstance.getState().isStop())) { @@ -2139,7 +2136,7 @@ public void forceProcessInstanceSuccessByTaskInstanceId(Integer taskInstanceId) List failTaskList = validTaskList.stream() .filter(instance -> instance.getState().isFailure() || instance.getState().isKill()) .map(TaskInstance::getId).collect(Collectors.toList()); - if (failTaskList.size() == 1 && failTaskList.contains(taskInstanceId)) { + if (failTaskList.size() == 1 && failTaskList.contains(task.getId())) { processInstance.setStateWithDesc(WorkflowExecutionStatus.SUCCESS, "success by task force success"); processInstanceDao.updateById(processInstance); } From ead54534a2061cc2daaf8ee24024a011a8b8ce4f Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Tue, 16 Apr 2024 17:27:36 +0800 Subject: [PATCH 032/165] Fix QUARTZ table order is not correct in initialization schema (#15857) --- .../resources/sql/dolphinscheduler_mysql.sql | 130 +++++++++--------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql index e2138b67a2c1..b30bc13887b8 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql @@ -15,7 +15,70 @@ * limitations under the License. */ -SET FOREIGN_KEY_CHECKS=0; +-- ---------------------------- +-- Table structure for QRTZ_JOB_DETAILS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`; +CREATE TABLE `QRTZ_JOB_DETAILS` ( + `SCHED_NAME` varchar(120) NOT NULL, + `JOB_NAME` varchar(200) NOT NULL, + `JOB_GROUP` varchar(200) NOT NULL, + `DESCRIPTION` varchar(250) DEFAULT NULL, + `JOB_CLASS_NAME` varchar(250) NOT NULL, + `IS_DURABLE` varchar(1) NOT NULL, + `IS_NONCONCURRENT` varchar(1) NOT NULL, + `IS_UPDATE_DATA` varchar(1) NOT NULL, + `REQUESTS_RECOVERY` varchar(1) NOT NULL, + `JOB_DATA` blob, + PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), + KEY `IDX_QRTZ_J_REQ_RECOVERY` (`SCHED_NAME`,`REQUESTS_RECOVERY`), + KEY `IDX_QRTZ_J_GRP` (`SCHED_NAME`,`JOB_GROUP`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE = utf8_bin; + +-- ---------------------------- +-- Records of QRTZ_JOB_DETAILS +-- ---------------------------- + +-- ---------------------------- +-- Table structure for QRTZ_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_TRIGGERS`; +CREATE TABLE `QRTZ_TRIGGERS` ( + `SCHED_NAME` varchar(120) NOT NULL, + `TRIGGER_NAME` varchar(200) NOT NULL, + `TRIGGER_GROUP` varchar(200) NOT NULL, + `JOB_NAME` varchar(200) NOT NULL, + `JOB_GROUP` varchar(200) NOT NULL, + `DESCRIPTION` varchar(250) DEFAULT NULL, + `NEXT_FIRE_TIME` bigint(13) DEFAULT NULL, + `PREV_FIRE_TIME` bigint(13) DEFAULT NULL, + `PRIORITY` int(11) DEFAULT NULL, + `TRIGGER_STATE` varchar(16) NOT NULL, + `TRIGGER_TYPE` varchar(8) NOT NULL, + `START_TIME` bigint(13) NOT NULL, + `END_TIME` bigint(13) DEFAULT NULL, + `CALENDAR_NAME` varchar(200) DEFAULT NULL, + `MISFIRE_INSTR` smallint(2) DEFAULT NULL, + `JOB_DATA` blob, + PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), + KEY `IDX_QRTZ_T_J` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), + KEY `IDX_QRTZ_T_JG` (`SCHED_NAME`,`JOB_GROUP`), + KEY `IDX_QRTZ_T_C` (`SCHED_NAME`,`CALENDAR_NAME`), + KEY `IDX_QRTZ_T_G` (`SCHED_NAME`,`TRIGGER_GROUP`), + KEY `IDX_QRTZ_T_STATE` (`SCHED_NAME`,`TRIGGER_STATE`), + KEY `IDX_QRTZ_T_N_STATE` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), + KEY `IDX_QRTZ_T_N_G_STATE` (`SCHED_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), + KEY `IDX_QRTZ_T_NEXT_FIRE_TIME` (`SCHED_NAME`,`NEXT_FIRE_TIME`), + KEY `IDX_QRTZ_T_NFT_ST` (`SCHED_NAME`,`TRIGGER_STATE`,`NEXT_FIRE_TIME`), + KEY `IDX_QRTZ_T_NFT_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`), + KEY `IDX_QRTZ_T_NFT_ST_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_STATE`), + KEY `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), + CONSTRAINT `QRTZ_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE = utf8_bin; + +-- ---------------------------- +-- Records of QRTZ_TRIGGERS +-- ---------------------------- -- ---------------------------- -- Table structure for QRTZ_BLOB_TRIGGERS @@ -99,30 +162,6 @@ CREATE TABLE `QRTZ_FIRED_TRIGGERS` ( -- Records of QRTZ_FIRED_TRIGGERS -- ---------------------------- --- ---------------------------- --- Table structure for QRTZ_JOB_DETAILS --- ---------------------------- -DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`; -CREATE TABLE `QRTZ_JOB_DETAILS` ( - `SCHED_NAME` varchar(120) NOT NULL, - `JOB_NAME` varchar(200) NOT NULL, - `JOB_GROUP` varchar(200) NOT NULL, - `DESCRIPTION` varchar(250) DEFAULT NULL, - `JOB_CLASS_NAME` varchar(250) NOT NULL, - `IS_DURABLE` varchar(1) NOT NULL, - `IS_NONCONCURRENT` varchar(1) NOT NULL, - `IS_UPDATE_DATA` varchar(1) NOT NULL, - `REQUESTS_RECOVERY` varchar(1) NOT NULL, - `JOB_DATA` blob, - PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), - KEY `IDX_QRTZ_J_REQ_RECOVERY` (`SCHED_NAME`,`REQUESTS_RECOVERY`), - KEY `IDX_QRTZ_J_GRP` (`SCHED_NAME`,`JOB_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE = utf8_bin; - --- ---------------------------- --- Records of QRTZ_JOB_DETAILS --- ---------------------------- - -- ---------------------------- -- Table structure for QRTZ_LOCKS -- ---------------------------- @@ -213,47 +252,6 @@ CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` ( -- Records of QRTZ_SIMPROP_TRIGGERS -- ---------------------------- --- ---------------------------- --- Table structure for QRTZ_TRIGGERS --- ---------------------------- -DROP TABLE IF EXISTS `QRTZ_TRIGGERS`; -CREATE TABLE `QRTZ_TRIGGERS` ( - `SCHED_NAME` varchar(120) NOT NULL, - `TRIGGER_NAME` varchar(200) NOT NULL, - `TRIGGER_GROUP` varchar(200) NOT NULL, - `JOB_NAME` varchar(200) NOT NULL, - `JOB_GROUP` varchar(200) NOT NULL, - `DESCRIPTION` varchar(250) DEFAULT NULL, - `NEXT_FIRE_TIME` bigint(13) DEFAULT NULL, - `PREV_FIRE_TIME` bigint(13) DEFAULT NULL, - `PRIORITY` int(11) DEFAULT NULL, - `TRIGGER_STATE` varchar(16) NOT NULL, - `TRIGGER_TYPE` varchar(8) NOT NULL, - `START_TIME` bigint(13) NOT NULL, - `END_TIME` bigint(13) DEFAULT NULL, - `CALENDAR_NAME` varchar(200) DEFAULT NULL, - `MISFIRE_INSTR` smallint(2) DEFAULT NULL, - `JOB_DATA` blob, - PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), - KEY `IDX_QRTZ_T_J` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), - KEY `IDX_QRTZ_T_JG` (`SCHED_NAME`,`JOB_GROUP`), - KEY `IDX_QRTZ_T_C` (`SCHED_NAME`,`CALENDAR_NAME`), - KEY `IDX_QRTZ_T_G` (`SCHED_NAME`,`TRIGGER_GROUP`), - KEY `IDX_QRTZ_T_STATE` (`SCHED_NAME`,`TRIGGER_STATE`), - KEY `IDX_QRTZ_T_N_STATE` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), - KEY `IDX_QRTZ_T_N_G_STATE` (`SCHED_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), - KEY `IDX_QRTZ_T_NEXT_FIRE_TIME` (`SCHED_NAME`,`NEXT_FIRE_TIME`), - KEY `IDX_QRTZ_T_NFT_ST` (`SCHED_NAME`,`TRIGGER_STATE`,`NEXT_FIRE_TIME`), - KEY `IDX_QRTZ_T_NFT_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`), - KEY `IDX_QRTZ_T_NFT_ST_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_STATE`), - KEY `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), - CONSTRAINT `QRTZ_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE = utf8_bin; - --- ---------------------------- --- Records of QRTZ_TRIGGERS --- ---------------------------- - -- ---------------------------- -- Table structure for t_ds_access_token -- ---------------------------- From 9437d276e76b18a2cf6c469fa886a027115ece03 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Tue, 16 Apr 2024 22:49:11 +0800 Subject: [PATCH 033/165] Change ssh heartbeat type to IGNORE (#15858) --- .../dolphinscheduler/plugin/datasource/ssh/SSHUtils.java | 2 +- .../plugin/task/remoteshell/RemoteExecutor.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHUtils.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHUtils.java index 42e1175e2efd..7abb18cee891 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHUtils.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHUtils.java @@ -59,7 +59,7 @@ public static ClientSession getSession(SshClient client, SSHConnectionParam conn throw new Exception("Failed to add public key identity", e); } } - session.setSessionHeartbeat(SessionHeartbeatController.HeartbeatType.RESERVED, Duration.ofSeconds(3)); + session.setSessionHeartbeat(SessionHeartbeatController.HeartbeatType.IGNORE, Duration.ofSeconds(3)); return session; } } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java index 334ebb61958f..307023043a4e 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java @@ -141,7 +141,6 @@ public Integer getTaskExitCode(String taskId) throws IOException { } } cleanData(taskId); - log.error("Remote shell task failed"); return exitCode; } @@ -232,8 +231,10 @@ public String runRemote(String command) throws IOException { channel.open(); channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0); channel.close(); - if (channel.getExitStatus() != 0) { - throw new TaskException("Remote shell task error, error message: " + err.toString()); + Integer exitStatus = channel.getExitStatus(); + if (exitStatus == null || exitStatus != 0) { + throw new TaskException( + "Remote shell task error, exitStatus: " + exitStatus + " error message: " + err); } return out.toString(); } From 9a48aca83c7275c934a08eab3c0bf4203a80149d Mon Sep 17 00:00:00 2001 From: zuo <58384836+xxzuo@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:18:29 +0800 Subject: [PATCH 034/165] [Fix-15866][Doc] update the taobao mirror link (#15867) --- docs/docs/en/contribute/frontend-development.md | 2 +- docs/docs/en/faq.md | 4 ++-- docs/docs/zh/contribute/frontend-development.md | 2 +- docs/docs/zh/faq.md | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/docs/en/contribute/frontend-development.md b/docs/docs/en/contribute/frontend-development.md index 0cc86811f898..50f8c91fb4b8 100644 --- a/docs/docs/en/contribute/frontend-development.md +++ b/docs/docs/en/contribute/frontend-development.md @@ -33,7 +33,7 @@ Use the command line mode `cd` enter the `dolphinscheduler-ui` project director > If `npm install` is very slow, you can set the taobao mirror ``` -npm config set registry http://registry.npm.taobao.org/ +npm config set registry http://registry.npmmirror.com/ ``` - Modify `API_BASE` in the file `dolphinscheduler-ui/.env` to interact with the backend: diff --git a/docs/docs/en/faq.md b/docs/docs/en/faq.md index b0954b3468fe..7ac4afb76e99 100644 --- a/docs/docs/en/faq.md +++ b/docs/docs/en/faq.md @@ -459,11 +459,11 @@ A: 1, cd dolphinscheduler-ui and delete node_modules directory sudo rm -rf node_modules ``` -​ 2, install node-sass through npm.taobao.org +​ 2, install node-sass through npmmirror.com ``` sudo npm uninstall node-sass -sudo npm i node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ +sudo npm i node-sass --sass_binary_site=https://npmmirror.com/mirrors/node-sass/ ``` 3, if the 2nd step failure, please, [referer url](https://github.com/apache/dolphinscheduler/blob/dev/docs/docs/en/contribute/frontend-development.md) diff --git a/docs/docs/zh/contribute/frontend-development.md b/docs/docs/zh/contribute/frontend-development.md index 42eb2973ddf1..249b17d546de 100644 --- a/docs/docs/zh/contribute/frontend-development.md +++ b/docs/docs/zh/contribute/frontend-development.md @@ -33,7 +33,7 @@ Node包下载 (注意版本 v12.20.2) `https://nodejs.org/download/release/v12.2 > 如果 `npm install` 速度非常慢,你可以设置淘宝镜像 ``` -npm config set registry http://registry.npm.taobao.org/ +npm config set registry http://registry.npmmirror.com/ ``` - 修改 `dolphinscheduler-ui/.env` 文件中的 `API_BASE`,用于跟后端交互: diff --git a/docs/docs/zh/faq.md b/docs/docs/zh/faq.md index 13e0dc8b84c0..a4f32d08a86a 100644 --- a/docs/docs/zh/faq.md +++ b/docs/docs/zh/faq.md @@ -430,11 +430,11 @@ A:1,cd dolphinscheduler-ui 然后删除 node_modules 目录 sudo rm -rf node_modules ``` -​ 2,通过 npm.taobao.org 下载 node-sass +​ 2,通过 npmmirror.com 下载 node-sass ``` sudo npm uninstall node-sass -sudo npm i node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ +sudo npm i node-sass --sass_binary_site=https://npmmirror.com/mirrors/node-sass/ ``` 3,如果步骤 2 报错,请重新构建 node-saas [参考链接](https://github.com/apache/dolphinscheduler/blob/dev/docs/docs/zh/contribute/frontend-development.md) From 3fe9fd45b12e2e6d190729e64b2c4663b0752ada Mon Sep 17 00:00:00 2001 From: XinXing <49302071+xinxingi@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:36:18 +0800 Subject: [PATCH 035/165] [Fix-15706] Seatunnel improvement (#15852) * fix_seatunnel_15706 * CodeFormat * Change to use JSONUtils * Constants moved to constant code * Delete empty lines * Delete empty lines --- .../common/utils/JSONUtils.java | 6 +- .../plugin/task/seatunnel/Constants.java | 2 + .../plugin/task/seatunnel/SeatunnelTask.java | 9 +- .../task/seatunnel/SeatunnelTaskTest.java | 93 +++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/test/org/apache/dolphinscheduler/plugin/task/seatunnel/SeatunnelTaskTest.java diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/JSONUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/JSONUtils.java index 6750b364f927..bfc3af2c58ef 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/JSONUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/JSONUtils.java @@ -196,7 +196,10 @@ public static List toList(String json, Class clazz) { * @return true if valid */ public static boolean checkJsonValid(String json) { + return checkJsonValid(json, true); + } + public static boolean checkJsonValid(String json, Boolean logFlag) { if (Strings.isNullOrEmpty(json)) { return false; } @@ -205,7 +208,8 @@ public static boolean checkJsonValid(String json) { objectMapper.readTree(json); return true; } catch (IOException e) { - log.error("check json object valid exception!", e); + if (logFlag) + log.error("check json object valid exception!", e); } return false; diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/java/org/apache/dolphinscheduler/plugin/task/seatunnel/Constants.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/java/org/apache/dolphinscheduler/plugin/task/seatunnel/Constants.java index fb1c52ee18ba..5f0f22b0a1ea 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/java/org/apache/dolphinscheduler/plugin/task/seatunnel/Constants.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/java/org/apache/dolphinscheduler/plugin/task/seatunnel/Constants.java @@ -29,5 +29,7 @@ private Constants() { public static final String STARTUP_SCRIPT_SPARK = "spark"; public static final String STARTUP_SCRIPT_FLINK = "flink"; public static final String STARTUP_SCRIPT_SEATUNNEL = "seatunnel"; + public static final String JSON_SUFFIX = "json"; + public static final String CONF_SUFFIX = "conf"; } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/java/org/apache/dolphinscheduler/plugin/task/seatunnel/SeatunnelTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/java/org/apache/dolphinscheduler/plugin/task/seatunnel/SeatunnelTask.java index 547e0158ef8c..2aadab8039fb 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/java/org/apache/dolphinscheduler/plugin/task/seatunnel/SeatunnelTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/java/org/apache/dolphinscheduler/plugin/task/seatunnel/SeatunnelTask.java @@ -184,8 +184,13 @@ private String buildCustomConfigContent() { } private String buildConfigFilePath() { - return String.format("%s/seatunnel_%s.conf", taskExecutionContext.getExecutePath(), - taskExecutionContext.getTaskAppId()); + return String.format("%s/seatunnel_%s.%s", taskExecutionContext.getExecutePath(), + taskExecutionContext.getTaskAppId(), formatDetector()); + } + + private String formatDetector() { + return JSONUtils.checkJsonValid(seatunnelParameters.getRawScript(), false) ? Constants.JSON_SUFFIX + : Constants.CONF_SUFFIX; } private void createConfigFileIfNotExists(String script, String scriptFile) throws IOException { diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/test/org/apache/dolphinscheduler/plugin/task/seatunnel/SeatunnelTaskTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/test/org/apache/dolphinscheduler/plugin/task/seatunnel/SeatunnelTaskTest.java new file mode 100644 index 000000000000..8f4c2a815a2a --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-seatunnel/src/main/test/org/apache/dolphinscheduler/plugin/task/seatunnel/SeatunnelTaskTest.java @@ -0,0 +1,93 @@ +/* + * 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.seatunnel; + +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; + +public class SeatunnelTaskTest { + private static final String EXECUTE_PATH = "/home"; + private static final String TASK_APPID = "9527"; + + @Test + public void formatDetector() throws Exception{ + SeatunnelParameters seatunnelParameters = new SeatunnelParameters(); + seatunnelParameters.setRawScript(RAW_SCRIPT); + + TaskExecutionContext taskExecutionContext = new TaskExecutionContext(); + taskExecutionContext.setExecutePath(EXECUTE_PATH); + taskExecutionContext.setTaskAppId(TASK_APPID); + taskExecutionContext.setTaskParams(JSONUtils.toJsonString(seatunnelParameters)); + + SeatunnelTask seatunnelTask = new SeatunnelTask(taskExecutionContext); + seatunnelTask.setSeatunnelParameters(seatunnelParameters); + Assertions.assertEquals("/home/seatunnel_9527.conf", seatunnelTask.buildCustomConfigCommand()); + + seatunnelParameters.setRawScript(RAW_SCRIPT_2); + seatunnelTask.setSeatunnelParameters(seatunnelParameters); + Assertions.assertEquals("/home/seatunnel_9527.json", seatunnelTask.buildCustomConfigCommand()); + } + private static final String RAW_SCRIPT = "env {\n" + + " execution.parallelism = 2\n" + + " job.mode = \"BATCH\"\n" + + " checkpoint.interval = 10000\n" + + "}\n" + + "\n" + + "source {\n" + + " FakeSource {\n" + + " parallelism = 2\n" + + " result_table_name = \"fake\"\n" + + " row.num = 16\n" + + " schema = {\n" + + " fields {\n" + + " name = \"string\"\n" + + " age = \"int\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n" + + "\n" + + "sink {\n" + + " Console {\n" + + " }\n" + + "}"; + private static final String RAW_SCRIPT_2 = "{\n" + + " \"env\": {\n" + + " \"execution.parallelism\": 2,\n" + + " \"job.mode\": \"BATCH\",\n" + + " \"checkpoint.interval\": 10000\n" + + " },\n" + + " \"source\": {\n" + + " \"FakeSource\": {\n" + + " \"parallelism\": 2,\n" + + " \"result_table_name\": \"fake\",\n" + + " \"row.num\": 16,\n" + + " \"schema\": {\n" + + " \"fields\": {\n" + + " \"name\": \"string\",\n" + + " \"age\": \"int\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"sink\": {\n" + + " \"Console\": {}\n" + + " }\n" + + "}"; +} \ No newline at end of file From a8bc23748d7e4371e962c095d40e06ce88fdf497 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 18 Apr 2024 11:31:11 +0800 Subject: [PATCH 036/165] Fix write audit log error will cause the request failed (#15868) --- .../api/audit/OperatorLogAspect.java | 16 ++++++++++-- .../api/audit/OperatorUtils.java | 1 - .../api/audit/operator/AuditOperator.java | 13 +++++++--- .../api/audit/operator/BaseAuditOperator.java | 26 +++++++++---------- .../api/service/AuditService.java | 10 ------- .../api/service/impl/AuditServiceImpl.java | 8 ------ 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorLogAspect.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorLogAspect.java index 4e1511298076..da8a7bd3e666 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorLogAspect.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorLogAspect.java @@ -18,9 +18,11 @@ package org.apache.dolphinscheduler.api.audit; import org.apache.dolphinscheduler.api.audit.operator.AuditOperator; +import org.apache.dolphinscheduler.api.utils.Result; import org.apache.dolphinscheduler.service.bean.SpringApplicationContext; import java.lang.reflect.Method; +import java.util.Map; import lombok.extern.slf4j.Slf4j; @@ -54,8 +56,18 @@ public Object around(ProceedingJoinPoint point) throws Throwable { log.warn("Operation is null of method: {}", method.getName()); return point.proceed(); } + long beginTime = System.currentTimeMillis(); - AuditOperator operator = SpringApplicationContext.getBean(operatorLog.auditType().getOperatorClass()); - return operator.recordAudit(point, operation.description(), operatorLog.auditType()); + Map paramsMap = OperatorUtils.getParamsMap(point, signature); + Result result = (Result) point.proceed(); + try { + AuditOperator operator = SpringApplicationContext.getBean(operatorLog.auditType().getOperatorClass()); + long latency = System.currentTimeMillis() - beginTime; + operator.recordAudit(paramsMap, result, latency, operation, operatorLog); + } catch (Throwable throwable) { + log.error("Record audit log error", throwable); + } + + return result; } } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java index a0233c7e93e3..10ccf8d64824 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java @@ -66,7 +66,6 @@ public static List buildAuditLogList(String apiDescription, AuditType auditLog.setOperationType(auditType.getAuditOperationType().getName()); auditLog.setDescription(apiDescription); auditLog.setCreateTime(new Date()); - auditLogList.add(auditLog); return auditLogList; } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/AuditOperator.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/AuditOperator.java index 6e1c01631360..7d76d5c4bd2e 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/AuditOperator.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/AuditOperator.java @@ -17,11 +17,18 @@ package org.apache.dolphinscheduler.api.audit.operator; -import org.apache.dolphinscheduler.api.audit.enums.AuditType; +import org.apache.dolphinscheduler.api.audit.OperatorLog; +import org.apache.dolphinscheduler.api.utils.Result; -import org.aspectj.lang.ProceedingJoinPoint; +import java.util.Map; + +import io.swagger.v3.oas.annotations.Operation; public interface AuditOperator { - Object recordAudit(ProceedingJoinPoint point, String describe, AuditType auditType) throws Throwable; + void recordAudit(Map paramsMap, + Result result, + long latency, + Operation operation, + OperatorLog operatorLog) throws Throwable; } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/BaseAuditOperator.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/BaseAuditOperator.java index 5c896a058a2f..270a5e4df41f 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/BaseAuditOperator.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/BaseAuditOperator.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.api.audit.operator; +import org.apache.dolphinscheduler.api.audit.OperatorLog; import org.apache.dolphinscheduler.api.audit.OperatorUtils; import org.apache.dolphinscheduler.api.audit.enums.AuditType; import org.apache.dolphinscheduler.api.service.AuditService; @@ -31,12 +32,11 @@ import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.google.common.base.Strings; +import io.swagger.v3.oas.annotations.Operation; @Service @Slf4j @@ -46,26 +46,27 @@ public abstract class BaseAuditOperator implements AuditOperator { private AuditService auditService; @Override - public Object recordAudit(ProceedingJoinPoint point, String describe, AuditType auditType) throws Throwable { - long beginTime = System.currentTimeMillis(); + public void recordAudit(Map paramsMap, + Result result, + long latency, + Operation operation, + OperatorLog operatorLog) { - MethodSignature signature = (MethodSignature) point.getSignature(); - Map paramsMap = OperatorUtils.getParamsMap(point, signature); + AuditType auditType = operatorLog.auditType(); User user = OperatorUtils.getUser(paramsMap); if (user == null) { log.error("user is null"); - return point.proceed(); + return; } - List auditLogList = OperatorUtils.buildAuditLogList(describe, auditType, user); + List auditLogList = OperatorUtils.buildAuditLogList(operation.description(), auditType, user); setRequestParam(auditType, auditLogList, paramsMap); - Result result = (Result) point.proceed(); if (OperatorUtils.resultFail(result)) { log.error("request fail, code {}", result.getCode()); - return result; + return; } setObjectIdentityFromReturnObject(auditType, result, auditLogList); @@ -73,10 +74,9 @@ public Object recordAudit(ProceedingJoinPoint point, String describe, AuditType modifyAuditOperationType(auditType, paramsMap, auditLogList); modifyAuditObjectType(auditType, paramsMap, auditLogList); - long latency = System.currentTimeMillis() - beginTime; - auditService.addAudit(auditLogList, latency); + auditLogList.forEach(auditLog -> auditLog.setLatency(latency)); + auditLogList.forEach(auditLog -> auditService.addAudit(auditLog)); - return result; } protected void setRequestParam(AuditType auditType, List auditLogList, Map paramsMap) { diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/AuditService.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/AuditService.java index b4296452f6d2..8fb99eaf735e 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/AuditService.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/AuditService.java @@ -21,8 +21,6 @@ import org.apache.dolphinscheduler.api.utils.PageInfo; import org.apache.dolphinscheduler.dao.entity.AuditLog; -import java.util.List; - /** * audit information service */ @@ -35,14 +33,6 @@ public interface AuditService { */ void addAudit(AuditLog auditLog); - /** - * add audit by list - * - * @param auditLogList auditLog list - * @param latency api latency milliseconds - */ - void addAudit(List auditLogList, long latency); - /** * query audit log list * diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AuditServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AuditServiceImpl.java index 2ec547bcb644..f8f763b4f4dd 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AuditServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/AuditServiceImpl.java @@ -57,14 +57,6 @@ public void addAudit(AuditLog auditLog) { auditLogMapper.insert(auditLog); } - @Override - public void addAudit(List auditLogList, long latency) { - auditLogList.forEach(auditLog -> { - auditLog.setLatency(latency); - addAudit(auditLog); - }); - } - /** * query audit log paging * From 163f8f01f0b3726374c81d9fa87da6571b3c460c Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 18 Apr 2024 15:43:28 +0800 Subject: [PATCH 037/165] Fix jdbc registry cannot work (#15861) --- .github/workflows/backend.yml | 12 +- .../Dockerfile | 6 +- .../mysql_with_mysql_registry/deploy.sh | 44 +++++ .../docker-compose-base.yaml | 34 ++++ .../docker-compose-cluster.yaml | 0 .../dolphinscheduler_env.sh | 58 +++++++ .../mysql_with_mysql_registry/install_env.sh | 58 +++++++ .../running_test.sh | 0 .../start-job.sh | 8 +- .../mysql_with_zookeeper_registry/Dockerfile | 48 ++++++ .../deploy.sh | 0 .../docker-compose-base.yaml | 0 .../docker-compose-cluster.yaml | 29 ++++ .../dolphinscheduler_env.sh | 0 .../install_env.sh | 0 .../running_test.sh | 108 +++++++++++++ .../start-job.sh | 33 ++++ .../Dockerfile | 39 +++++ .../deploy.sh | 41 +++++ .../docker-compose-base.yaml | 35 ++++ .../docker-compose-cluster.yaml | 0 .../dolphinscheduler_env.sh | 58 +++++++ .../install_env.sh | 58 +++++++ .../running_test.sh | 0 .../start-job.sh | 33 ++++ .../Dockerfile | 6 +- .../deploy.sh | 0 .../docker-compose-base.yaml | 0 .../docker-compose-cluster.yaml | 29 ++++ .../dolphinscheduler_env.sh | 0 .../install_env.sh | 0 .../running_test.sh | 109 +++++++++++++ .../start-job.sh | 8 +- .../dolphinscheduler/alert/AlertServer.java | 19 ++- .../api/ApiApplicationServer.java | 6 +- .../dolphinscheduler/dao/PluginDao.java | 4 +- .../{ => repository/impl}/AlertDaoTest.java | 4 +- .../server/master/MasterServer.java | 6 +- .../dolphinscheduler-registry-jdbc/README.md | 10 +- .../dolphinscheduler-registry-jdbc/pom.xml | 10 ++ ...ava => JdbcRegistryAutoConfiguration.java} | 30 +++- .../main/resources/META-INF/spring.factories | 19 +++ .../main/resources/mysql_registry_init.sql | 1 - .../src/main/bin/initialize-jdbc-registry.sh | 31 ++++ .../tools/command/CommandApplication.java | 152 ++++++++++++++++++ .../src/main/resources/application.yaml | 2 + .../server/worker/WorkerServer.java | 6 +- 47 files changed, 1111 insertions(+), 43 deletions(-) rename .github/workflows/cluster-test/{mysql => mysql_with_mysql_registry}/Dockerfile (85%) create mode 100644 .github/workflows/cluster-test/mysql_with_mysql_registry/deploy.sh create mode 100644 .github/workflows/cluster-test/mysql_with_mysql_registry/docker-compose-base.yaml rename .github/workflows/cluster-test/{mysql => mysql_with_mysql_registry}/docker-compose-cluster.yaml (100%) create mode 100755 .github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh create mode 100644 .github/workflows/cluster-test/mysql_with_mysql_registry/install_env.sh rename .github/workflows/cluster-test/{mysql => mysql_with_mysql_registry}/running_test.sh (100%) rename .github/workflows/cluster-test/{mysql => mysql_with_mysql_registry}/start-job.sh (74%) create mode 100644 .github/workflows/cluster-test/mysql_with_zookeeper_registry/Dockerfile rename .github/workflows/cluster-test/{mysql => mysql_with_zookeeper_registry}/deploy.sh (100%) rename .github/workflows/cluster-test/{mysql => mysql_with_zookeeper_registry}/docker-compose-base.yaml (100%) create mode 100644 .github/workflows/cluster-test/mysql_with_zookeeper_registry/docker-compose-cluster.yaml rename .github/workflows/cluster-test/{mysql => mysql_with_zookeeper_registry}/dolphinscheduler_env.sh (100%) rename .github/workflows/cluster-test/{mysql => mysql_with_zookeeper_registry}/install_env.sh (100%) create mode 100644 .github/workflows/cluster-test/mysql_with_zookeeper_registry/running_test.sh create mode 100644 .github/workflows/cluster-test/mysql_with_zookeeper_registry/start-job.sh create mode 100644 .github/workflows/cluster-test/postgresql_with_postgresql_registry/Dockerfile create mode 100644 .github/workflows/cluster-test/postgresql_with_postgresql_registry/deploy.sh create mode 100644 .github/workflows/cluster-test/postgresql_with_postgresql_registry/docker-compose-base.yaml rename .github/workflows/cluster-test/{postgresql => postgresql_with_postgresql_registry}/docker-compose-cluster.yaml (100%) create mode 100644 .github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh create mode 100644 .github/workflows/cluster-test/postgresql_with_postgresql_registry/install_env.sh rename .github/workflows/cluster-test/{postgresql => postgresql_with_postgresql_registry}/running_test.sh (100%) create mode 100644 .github/workflows/cluster-test/postgresql_with_postgresql_registry/start-job.sh rename .github/workflows/cluster-test/{postgresql => postgresql_with_zookeeper_registry}/Dockerfile (77%) rename .github/workflows/cluster-test/{postgresql => postgresql_with_zookeeper_registry}/deploy.sh (100%) rename .github/workflows/cluster-test/{postgresql => postgresql_with_zookeeper_registry}/docker-compose-base.yaml (100%) create mode 100644 .github/workflows/cluster-test/postgresql_with_zookeeper_registry/docker-compose-cluster.yaml rename .github/workflows/cluster-test/{postgresql => postgresql_with_zookeeper_registry}/dolphinscheduler_env.sh (100%) rename .github/workflows/cluster-test/{postgresql => postgresql_with_zookeeper_registry}/install_env.sh (100%) create mode 100644 .github/workflows/cluster-test/postgresql_with_zookeeper_registry/running_test.sh rename .github/workflows/cluster-test/{postgresql => postgresql_with_zookeeper_registry}/start-job.sh (73%) rename dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/{ => repository/impl}/AlertDaoTest.java (95%) rename dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/{JdbcRegistryConfiguration.java => JdbcRegistryAutoConfiguration.java} (65%) create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/resources/META-INF/spring.factories create mode 100644 dolphinscheduler-tools/src/main/bin/initialize-jdbc-registry.sh create mode 100644 dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/command/CommandApplication.java diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index ea09a17fd2ae..671223d28608 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -106,10 +106,14 @@ jobs: strategy: matrix: case: - - name: cluster-test-mysql - script: .github/workflows/cluster-test/mysql/start-job.sh - - name: cluster-test-postgresql - script: .github/workflows/cluster-test/postgresql/start-job.sh + - name: cluster-test-mysql-with-zookeeper-registry + script: .github/workflows/cluster-test/mysql_with_zookeeper_registry/start-job.sh + - name: cluster-test-mysql-with-mysql-registry + script: .github/workflows/cluster-test/mysql_with_mysql_registry/start-job.sh + - name: cluster-test-postgresql-zookeeper-registry + script: .github/workflows/cluster-test/postgresql_with_zookeeper_registry/start-job.sh + - name: cluster-test-postgresql-with-postgresql-registry + script: .github/workflows/cluster-test/postgresql_with_postgresql_registry/start-job.sh steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/cluster-test/mysql/Dockerfile b/.github/workflows/cluster-test/mysql_with_mysql_registry/Dockerfile similarity index 85% rename from .github/workflows/cluster-test/mysql/Dockerfile rename to .github/workflows/cluster-test/mysql_with_mysql_registry/Dockerfile index c7d6abe8890b..12c7db3c187b 100644 --- a/.github/workflows/cluster-test/mysql/Dockerfile +++ b/.github/workflows/cluster-test/mysql_with_mysql_registry/Dockerfile @@ -28,10 +28,10 @@ RUN mv /root/apache-dolphinscheduler-*-SNAPSHOT-bin /root/apache-dolphinschedule ENV DOLPHINSCHEDULER_HOME /root/apache-dolphinscheduler-test-SNAPSHOT-bin #Setting install.sh -COPY .github/workflows/cluster-test/mysql/install_env.sh $DOLPHINSCHEDULER_HOME/bin/env/install_env.sh +COPY .github/workflows/cluster-test/mysql_with_mysql_registry/install_env.sh $DOLPHINSCHEDULER_HOME/bin/env/install_env.sh #Setting dolphinscheduler_env.sh -COPY .github/workflows/cluster-test/mysql/dolphinscheduler_env.sh $DOLPHINSCHEDULER_HOME/bin/env/dolphinscheduler_env.sh +COPY .github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh $DOLPHINSCHEDULER_HOME/bin/env/dolphinscheduler_env.sh #Download mysql jar ENV MYSQL_URL "https://repo.maven.apache.org/maven2/mysql/mysql-connector-java/8.0.16/mysql-connector-java-8.0.16.jar" @@ -43,6 +43,6 @@ cp $DOLPHINSCHEDULER_HOME/alert-server/libs/$MYSQL_DRIVER $DOLPHINSCHEDULER_HOME cp $DOLPHINSCHEDULER_HOME/alert-server/libs/$MYSQL_DRIVER $DOLPHINSCHEDULER_HOME/tools/libs/$MYSQL_DRIVER #Deploy -COPY .github/workflows/cluster-test/mysql/deploy.sh /root/deploy.sh +COPY .github/workflows/cluster-test/mysql_with_mysql_registry/deploy.sh /root/deploy.sh CMD [ "/bin/bash", "/root/deploy.sh" ] diff --git a/.github/workflows/cluster-test/mysql_with_mysql_registry/deploy.sh b/.github/workflows/cluster-test/mysql_with_mysql_registry/deploy.sh new file mode 100644 index 000000000000..72b2a630faba --- /dev/null +++ b/.github/workflows/cluster-test/mysql_with_mysql_registry/deploy.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# +# 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. +# +set -euox pipefail + + +USER=root + +#Create database +mysql -hmysql -P3306 -uroot -p123456 -e "CREATE DATABASE IF NOT EXISTS dolphinscheduler DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;" + +#Sudo +sed -i '$a'$USER' ALL=(ALL) NOPASSWD: NOPASSWD: ALL' /etc/sudoers +sed -i 's/Defaults requirett/#Defaults requirett/g' /etc/sudoers + +#SSH +ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa +cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys +chmod 600 ~/.ssh/authorized_keys +service ssh start + +#Init schema +/bin/bash $DOLPHINSCHEDULER_HOME/tools/bin/upgrade-schema.sh +/bin/bash $DOLPHINSCHEDULER_HOME/tools/bin/initialize-jdbc-registry.sh + +#Start Cluster +/bin/bash $DOLPHINSCHEDULER_HOME/bin/start-all.sh + +#Keep running +tail -f /dev/null diff --git a/.github/workflows/cluster-test/mysql_with_mysql_registry/docker-compose-base.yaml b/.github/workflows/cluster-test/mysql_with_mysql_registry/docker-compose-base.yaml new file mode 100644 index 000000000000..d59e3c868ce1 --- /dev/null +++ b/.github/workflows/cluster-test/mysql_with_mysql_registry/docker-compose-base.yaml @@ -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. +# + +version: "3" + +services: + mysql: + container_name: mysql + image: mysql:5.7.36 + command: --default-authentication-plugin=mysql_native_password + restart: always + environment: + MYSQL_ROOT_PASSWORD: 123456 + ports: + - "3306:3306" + healthcheck: + test: mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD + interval: 5s + timeout: 60s + retries: 120 diff --git a/.github/workflows/cluster-test/mysql/docker-compose-cluster.yaml b/.github/workflows/cluster-test/mysql_with_mysql_registry/docker-compose-cluster.yaml similarity index 100% rename from .github/workflows/cluster-test/mysql/docker-compose-cluster.yaml rename to .github/workflows/cluster-test/mysql_with_mysql_registry/docker-compose-cluster.yaml diff --git a/.github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh b/.github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh new file mode 100755 index 000000000000..58937e740c14 --- /dev/null +++ b/.github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh @@ -0,0 +1,58 @@ +# +# 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. +# + +# JAVA_HOME, will use it to start DolphinScheduler server +export JAVA_HOME=${JAVA_HOME:-/opt/java/openjdk} + +# Database related configuration, set database type, username and password +export DATABASE=${DATABASE:-mysql} +export SPRING_PROFILES_ACTIVE=${DATABASE} +export SPRING_DATASOURCE_URL="jdbc:mysql://mysql:3306/dolphinscheduler?useUnicode=true&characterEncoding=UTF-8&useSSL=false" +export SPRING_DATASOURCE_USERNAME=root +export SPRING_DATASOURCE_PASSWORD=123456 + +# DolphinScheduler server related configuration +export SPRING_CACHE_TYPE=${SPRING_CACHE_TYPE:-none} +export SPRING_JACKSON_TIME_ZONE=${SPRING_JACKSON_TIME_ZONE:-UTC} +export MASTER_FETCH_COMMAND_NUM=${MASTER_FETCH_COMMAND_NUM:-10} + +# Registry center configuration, determines the type and link of the registry center +export REGISTRY_TYPE=${REGISTRY_TYPE:-jdbc} +export REGISTRY_HIKARI_CONFIG_JDBC_URL="jdbc:mysql://mysql:3306/dolphinscheduler?useUnicode=true&characterEncoding=UTF-8&useSSL=false" +export REGISTRY_HIKARI_CONFIG_USERNAME=root +export REGISTRY_HIKARI_CONFIG_PASSWORD=123456 + +# Tasks related configurations, need to change the configuration if you use the related tasks. +export HADOOP_HOME=${HADOOP_HOME:-/opt/soft/hadoop} +export HADOOP_CONF_DIR=${HADOOP_CONF_DIR:-/opt/soft/hadoop/etc/hadoop} +export SPARK_HOME=${SPARK_HOME:-/opt/soft/spark} +export PYTHON_LAUNCHER=${PYTHON_LAUNCHER:-/opt/soft/python/bin/python3} +export HIVE_HOME=${HIVE_HOME:-/opt/soft/hive} +export FLINK_HOME=${FLINK_HOME:-/opt/soft/flink} +export DATAX_LAUNCHER=${DATAX_LAUNCHER:-/opt/soft/datax/bin/datax.py} + +export PATH=$HADOOP_HOME/bin:$SPARK_HOME/bin:$PYTHON_LAUNCHER:$JAVA_HOME/bin:$HIVE_HOME/bin:$FLINK_HOME/bin:$DATAX_LAUNCHER:$PATH + +export MASTER_RESERVED_MEMORY=0.01 +export WORKER_RESERVED_MEMORY=0.01 + +# applicationId auto collection related configuration, the following configurations are unnecessary if setting appId.collect=log +#export HADOOP_CLASSPATH=`hadoop classpath`:${DOLPHINSCHEDULER_HOME}/tools/libs/* +#export SPARK_DIST_CLASSPATH=$HADOOP_CLASSPATH:$SPARK_DIST_CLASS_PATH +#export HADOOP_CLIENT_OPTS="-javaagent:${DOLPHINSCHEDULER_HOME}/tools/libs/aspectjweaver-1.9.7.jar":$HADOOP_CLIENT_OPTS +#export SPARK_SUBMIT_OPTS="-javaagent:${DOLPHINSCHEDULER_HOME}/tools/libs/aspectjweaver-1.9.7.jar":$SPARK_SUBMIT_OPTS +#export FLINK_ENV_JAVA_OPTS="-javaagent:${DOLPHINSCHEDULER_HOME}/tools/libs/aspectjweaver-1.9.7.jar":$FLINK_ENV_JAVA_OPTS diff --git a/.github/workflows/cluster-test/mysql_with_mysql_registry/install_env.sh b/.github/workflows/cluster-test/mysql_with_mysql_registry/install_env.sh new file mode 100644 index 000000000000..cd660febf88b --- /dev/null +++ b/.github/workflows/cluster-test/mysql_with_mysql_registry/install_env.sh @@ -0,0 +1,58 @@ +# +# 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. +# + +# --------------------------------------------------------- +# INSTALL MACHINE +# --------------------------------------------------------- +# A comma separated list of machine hostname or IP would be installed DolphinScheduler, +# including master, worker, api, alert. If you want to deploy in pseudo-distributed +# mode, just write a pseudo-distributed hostname +# Example for hostnames: ips="ds1,ds2,ds3,ds4,ds5", Example for IPs: ips="192.168.8.1,192.168.8.2,192.168.8.3,192.168.8.4,192.168.8.5" +ips=${ips:-"localhost"} + +# Port of SSH protocol, default value is 22. For now we only support same port in all `ips` machine +# modify it if you use different ssh port +sshPort=${sshPort:-"22"} + +# A comma separated list of machine hostname or IP would be installed Master server, it +# must be a subset of configuration `ips`. +# Example for hostnames: masters="ds1,ds2", Example for IPs: masters="192.168.8.1,192.168.8.2" +masters=${masters:-"localhost"} + +# A comma separated list of machine : or :.All hostname or IP must be a +# subset of configuration `ips`, And workerGroup have default value as `default`, but we recommend you declare behind the hosts +# Example for hostnames: workers="ds1:default,ds2:default,ds3:default", Example for IPs: workers="192.168.8.1:default,192.168.8.2:default,192.168.8.3:default" +workers=${workers:-"localhost:default"} + +# A comma separated list of machine hostname or IP would be installed Alert server, it +# must be a subset of configuration `ips`. +# Example for hostname: alertServer="ds3", Example for IP: alertServer="192.168.8.3" +alertServer=${alertServer:-"localhost"} + +# A comma separated list of machine hostname or IP would be installed API server, it +# must be a subset of configuration `ips`. +# Example for hostname: apiServers="ds1", Example for IP: apiServers="192.168.8.1" +apiServers=${apiServers:-"localhost"} + +# The directory to install DolphinScheduler for all machine we config above. It will automatically be created by `install.sh` script if not exists. +# Do not set this configuration same as the current path (pwd) +installPath=${installPath:-"/root/apache-dolphinscheduler-*-SNAPSHOT-bin"} + +# The user to deploy DolphinScheduler for all machine we config above. For now user must create by yourself before running `install.sh` +# script. The user needs to have sudo privileges and permissions to operate hdfs. If hdfs is enabled than the root directory needs +# to be created by this user +deployUser=${deployUser:-"dolphinscheduler"} diff --git a/.github/workflows/cluster-test/mysql/running_test.sh b/.github/workflows/cluster-test/mysql_with_mysql_registry/running_test.sh similarity index 100% rename from .github/workflows/cluster-test/mysql/running_test.sh rename to .github/workflows/cluster-test/mysql_with_mysql_registry/running_test.sh diff --git a/.github/workflows/cluster-test/mysql/start-job.sh b/.github/workflows/cluster-test/mysql_with_mysql_registry/start-job.sh similarity index 74% rename from .github/workflows/cluster-test/mysql/start-job.sh rename to .github/workflows/cluster-test/mysql_with_mysql_registry/start-job.sh index ee67c5179b7f..0ce48c64ae9b 100644 --- a/.github/workflows/cluster-test/mysql/start-job.sh +++ b/.github/workflows/cluster-test/mysql_with_mysql_registry/start-job.sh @@ -18,16 +18,16 @@ set -euox pipefail #Start base service containers -docker-compose -f .github/workflows/cluster-test/mysql/docker-compose-base.yaml up -d +docker-compose -f .github/workflows/cluster-test/mysql_with_mysql_registry/docker-compose-base.yaml up -d #Build ds mysql cluster image -docker build -t jdk8:ds_mysql_cluster -f .github/workflows/cluster-test/mysql/Dockerfile . +docker build -t jdk8:ds_mysql_cluster -f .github/workflows/cluster-test/mysql_with_mysql_registry/Dockerfile . #Start ds mysql cluster container -docker-compose -f .github/workflows/cluster-test/mysql/docker-compose-cluster.yaml up -d +docker-compose -f .github/workflows/cluster-test/mysql_with_mysql_registry/docker-compose-cluster.yaml up -d #Running tests -/bin/bash .github/workflows/cluster-test/mysql/running_test.sh +/bin/bash .github/workflows/cluster-test/mysql_with_mysql_registry/running_test.sh #Cleanup docker rm -f $(docker ps -aq) diff --git a/.github/workflows/cluster-test/mysql_with_zookeeper_registry/Dockerfile b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/Dockerfile new file mode 100644 index 000000000000..574c05944239 --- /dev/null +++ b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/Dockerfile @@ -0,0 +1,48 @@ +# +# 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. +# + +FROM eclipse-temurin:8-jre + +RUN apt update ; \ + apt install -y wget default-mysql-client sudo openssh-server netcat-traditional ; + +COPY ./apache-dolphinscheduler-*-SNAPSHOT-bin.tar.gz /root +RUN tar -zxvf /root/apache-dolphinscheduler-*-SNAPSHOT-bin.tar.gz -C ~ + +RUN mv /root/apache-dolphinscheduler-*-SNAPSHOT-bin /root/apache-dolphinscheduler-test-SNAPSHOT-bin + +ENV DOLPHINSCHEDULER_HOME /root/apache-dolphinscheduler-test-SNAPSHOT-bin + +#Setting install.sh +COPY .github/workflows/cluster-test/mysql_with_zookeeper_registry/install_env.sh $DOLPHINSCHEDULER_HOME/bin/env/install_env.sh + +#Setting dolphinscheduler_env.sh +COPY .github/workflows/cluster-test/mysql_with_zookeeper_registry/dolphinscheduler_env.sh $DOLPHINSCHEDULER_HOME/bin/env/dolphinscheduler_env.sh + +#Download mysql jar +ENV MYSQL_URL "https://repo.maven.apache.org/maven2/mysql/mysql-connector-java/8.0.16/mysql-connector-java-8.0.16.jar" +ENV MYSQL_DRIVER "mysql-connector-java-8.0.16.jar" +RUN wget -O $DOLPHINSCHEDULER_HOME/alert-server/libs/$MYSQL_DRIVER $MYSQL_URL ; \ +cp $DOLPHINSCHEDULER_HOME/alert-server/libs/$MYSQL_DRIVER $DOLPHINSCHEDULER_HOME/api-server/libs/$MYSQL_DRIVER ; \ +cp $DOLPHINSCHEDULER_HOME/alert-server/libs/$MYSQL_DRIVER $DOLPHINSCHEDULER_HOME/master-server/libs/$MYSQL_DRIVER ; \ +cp $DOLPHINSCHEDULER_HOME/alert-server/libs/$MYSQL_DRIVER $DOLPHINSCHEDULER_HOME/worker-server/libs/$MYSQL_DRIVER ; \ +cp $DOLPHINSCHEDULER_HOME/alert-server/libs/$MYSQL_DRIVER $DOLPHINSCHEDULER_HOME/tools/libs/$MYSQL_DRIVER + +#Deploy +COPY .github/workflows/cluster-test/mysql_with_zookeeper_registry/deploy.sh /root/deploy.sh + +CMD [ "/bin/bash", "/root/deploy.sh" ] diff --git a/.github/workflows/cluster-test/mysql/deploy.sh b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/deploy.sh similarity index 100% rename from .github/workflows/cluster-test/mysql/deploy.sh rename to .github/workflows/cluster-test/mysql_with_zookeeper_registry/deploy.sh diff --git a/.github/workflows/cluster-test/mysql/docker-compose-base.yaml b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/docker-compose-base.yaml similarity index 100% rename from .github/workflows/cluster-test/mysql/docker-compose-base.yaml rename to .github/workflows/cluster-test/mysql_with_zookeeper_registry/docker-compose-base.yaml diff --git a/.github/workflows/cluster-test/mysql_with_zookeeper_registry/docker-compose-cluster.yaml b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/docker-compose-cluster.yaml new file mode 100644 index 000000000000..7343c8eee71a --- /dev/null +++ b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/docker-compose-cluster.yaml @@ -0,0 +1,29 @@ +# +# 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" + +services: + ds: + container_name: ds + image: jdk8:ds_mysql_cluster + restart: always + ports: + - "12345:12345" + - "5679:5679" + - "1235:1235" + - "50053:50053" diff --git a/.github/workflows/cluster-test/mysql/dolphinscheduler_env.sh b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/dolphinscheduler_env.sh similarity index 100% rename from .github/workflows/cluster-test/mysql/dolphinscheduler_env.sh rename to .github/workflows/cluster-test/mysql_with_zookeeper_registry/dolphinscheduler_env.sh diff --git a/.github/workflows/cluster-test/mysql/install_env.sh b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/install_env.sh similarity index 100% rename from .github/workflows/cluster-test/mysql/install_env.sh rename to .github/workflows/cluster-test/mysql_with_zookeeper_registry/install_env.sh diff --git a/.github/workflows/cluster-test/mysql_with_zookeeper_registry/running_test.sh b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/running_test.sh new file mode 100644 index 000000000000..7582c3ccc5ea --- /dev/null +++ b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/running_test.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# +# 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. +# +set -x + + +API_HEALTHCHECK_COMMAND="curl -I -m 10 -o /dev/null -s -w %{http_code} http://0.0.0.0:12345/dolphinscheduler/actuator/health" +MASTER_HEALTHCHECK_COMMAND="curl -I -m 10 -o /dev/null -s -w %{http_code} http://0.0.0.0:5679/actuator/health" +WORKER_HEALTHCHECK_COMMAND="curl -I -m 10 -o /dev/null -s -w %{http_code} http://0.0.0.0:1235/actuator/health" +ALERT_HEALTHCHECK_COMMAND="curl -I -m 10 -o /dev/null -s -w %{http_code} http://0.0.0.0:50053/actuator/health" + +#Cluster start health check +TIMEOUT=180 +START_HEALTHCHECK_EXITCODE=0 + +for ((i=1; i<=TIMEOUT; i++)) +do + MASTER_HTTP_STATUS=$(eval "$MASTER_HEALTHCHECK_COMMAND") + WORKER_HTTP_STATUS=$(eval "$WORKER_HEALTHCHECK_COMMAND") + API_HTTP_STATUS=$(eval "$API_HEALTHCHECK_COMMAND") + ALERT_HTTP_STATUS=$(eval "$ALERT_HEALTHCHECK_COMMAND") + if [[ $MASTER_HTTP_STATUS -eq 200 && $WORKER_HTTP_STATUS -eq 200 && $API_HTTP_STATUS -eq 200 && $ALERT_HTTP_STATUS -eq 200 ]];then + START_HEALTHCHECK_EXITCODE=0 + else + START_HEALTHCHECK_EXITCODE=2 + fi + + if [[ $START_HEALTHCHECK_EXITCODE -eq 0 ]];then + echo "cluster start health check success" + break + fi + + if [[ $i -eq $TIMEOUT ]];then + if [[ $MASTER_HTTP_STATUS -ne 200 ]];then + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/master-server/logs/dolphinscheduler-master.log" + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/master-server/logs/*.out" + echo "master start health check failed" + fi + if [[ $WORKER_HTTP_STATUS -ne 200 ]]; then + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/worker-server/logs/dolphinscheduler-worker.log" + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/worker-server/logs/*.out" + echo "worker start health check failed" + fi + if [[ $API_HTTP_STATUS -ne 200 ]]; then + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/api-server/logs/dolphinscheduler-api.log" + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/api-server/logs/*.out" + echo "api start health check failed" + fi + if [[ $ALERT_HTTP_STATUS -ne 200 ]]; then + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/alert-server/logs/dolphinscheduler-alert.log" + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/alert-server/logs/*.out" + echo "alert start health check failed" + fi + exit $START_HEALTHCHECK_EXITCODE + fi + sleep 1 +done + +#Stop Cluster +docker exec -u root ds bash -c "/root/apache-dolphinscheduler-*-SNAPSHOT-bin/bin/stop-all.sh" + +#Cluster stop health check +sleep 5 +MASTER_HTTP_STATUS=$(eval "$MASTER_HEALTHCHECK_COMMAND") +if [[ $MASTER_HTTP_STATUS -ne 200 ]];then + echo "master stop health check success" +else + echo "master stop health check failed" + exit 3 +fi + +WORKER_HTTP_STATUS=$(eval "$WORKER_HEALTHCHECK_COMMAND") +if [[ $WORKER_HTTP_STATUS -ne 200 ]];then + echo "worker stop health check success" +else + echo "worker stop health check failed" + exit 3 +fi + +API_HTTP_STATUS=$(eval "$API_HEALTHCHECK_COMMAND") +if [[ $API_HTTP_STATUS -ne 200 ]];then + echo "api stop health check success" +else + echo "api stop health check failed" + exit 3 +fi + +ALERT_HTTP_STATUS=$(eval "$ALERT_HEALTHCHECK_COMMAND") +if [[ $ALERT_HTTP_STATUS -ne 200 ]];then + echo "alert stop health check success" +else + echo "alert stop health check failed" + exit 3 +fi diff --git a/.github/workflows/cluster-test/mysql_with_zookeeper_registry/start-job.sh b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/start-job.sh new file mode 100644 index 000000000000..db8d23147ed3 --- /dev/null +++ b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/start-job.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# +# 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. +# +set -euox pipefail + +#Start base service containers +docker-compose -f .github/workflows/cluster-test/mysql_with_zookeeper_registry/docker-compose-base.yaml up -d + +#Build ds mysql cluster image +docker build -t jdk8:ds_mysql_cluster -f .github/workflows/cluster-test/mysql_with_zookeeper_registry/Dockerfile . + +#Start ds mysql cluster container +docker-compose -f .github/workflows/cluster-test/mysql_with_zookeeper_registry/docker-compose-cluster.yaml up -d + +#Running tests +/bin/bash .github/workflows/cluster-test/mysql_with_zookeeper_registry/running_test.sh + +#Cleanup +docker rm -f $(docker ps -aq) diff --git a/.github/workflows/cluster-test/postgresql_with_postgresql_registry/Dockerfile b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/Dockerfile new file mode 100644 index 000000000000..bb2d9a5383d0 --- /dev/null +++ b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/Dockerfile @@ -0,0 +1,39 @@ +# +# 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. +# + +FROM eclipse-temurin:8-jre + +RUN apt update ; \ + apt install -y wget sudo openssh-server netcat-traditional ; + +COPY ./apache-dolphinscheduler-*-SNAPSHOT-bin.tar.gz /root +RUN tar -zxvf /root/apache-dolphinscheduler-*-SNAPSHOT-bin.tar.gz -C ~ + +RUN mv /root/apache-dolphinscheduler-*-SNAPSHOT-bin /root/apache-dolphinscheduler-test-SNAPSHOT-bin + +ENV DOLPHINSCHEDULER_HOME /root/apache-dolphinscheduler-test-SNAPSHOT-bin + +#Setting install.sh +COPY .github/workflows/cluster-test/postgresql_with_postgresql_registry/install_env.sh $DOLPHINSCHEDULER_HOME/bin/env/install_env.sh + +#Setting dolphinscheduler_env.sh +COPY .github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh $DOLPHINSCHEDULER_HOME/bin/env/dolphinscheduler_env.sh + +#Deploy +COPY .github/workflows/cluster-test/postgresql_with_postgresql_registry/deploy.sh /root/deploy.sh + +CMD [ "/bin/bash", "/root/deploy.sh" ] diff --git a/.github/workflows/cluster-test/postgresql_with_postgresql_registry/deploy.sh b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/deploy.sh new file mode 100644 index 000000000000..37bf3433c014 --- /dev/null +++ b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/deploy.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# +# 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. +# +set -euox pipefail + + +USER=root + +#Sudo +sed -i '$a'$USER' ALL=(ALL) NOPASSWD: NOPASSWD: ALL' /etc/sudoers +sed -i 's/Defaults requirett/#Defaults requirett/g' /etc/sudoers + +#SSH +ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa +cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys +chmod 600 ~/.ssh/authorized_keys +service ssh start + +#Init schema +/bin/bash $DOLPHINSCHEDULER_HOME/tools/bin/upgrade-schema.sh +/bin/bash $DOLPHINSCHEDULER_HOME/tools/bin/initialize-jdbc-registry.sh + +#Start Cluster +/bin/bash $DOLPHINSCHEDULER_HOME/bin/start-all.sh + +#Keep running +tail -f /dev/null diff --git a/.github/workflows/cluster-test/postgresql_with_postgresql_registry/docker-compose-base.yaml b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/docker-compose-base.yaml new file mode 100644 index 000000000000..1793d94f39dc --- /dev/null +++ b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/docker-compose-base.yaml @@ -0,0 +1,35 @@ +# +# 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" + +services: + postgres: + container_name: postgres + image: postgres:14.1 + restart: always + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: dolphinscheduler + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 60s + retries: 120 + diff --git a/.github/workflows/cluster-test/postgresql/docker-compose-cluster.yaml b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/docker-compose-cluster.yaml similarity index 100% rename from .github/workflows/cluster-test/postgresql/docker-compose-cluster.yaml rename to .github/workflows/cluster-test/postgresql_with_postgresql_registry/docker-compose-cluster.yaml diff --git a/.github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh new file mode 100644 index 000000000000..e7fd1b7204a5 --- /dev/null +++ b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh @@ -0,0 +1,58 @@ +# +# 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. +# + +# JAVA_HOME, will use it to start DolphinScheduler server +export JAVA_HOME=${JAVA_HOME:-/opt/java/openjdk} + +# Database related configuration, set database type, username and password +export DATABASE=${DATABASE:-postgresql} +export SPRING_PROFILES_ACTIVE=${DATABASE} +export SPRING_DATASOURCE_URL="jdbc:postgresql://postgres:5432/dolphinscheduler" +export SPRING_DATASOURCE_USERNAME=postgres +export SPRING_DATASOURCE_PASSWORD=postgres + +# DolphinScheduler server related configuration +export SPRING_CACHE_TYPE=${SPRING_CACHE_TYPE:-none} +export SPRING_JACKSON_TIME_ZONE=${SPRING_JACKSON_TIME_ZONE:-UTC} +export MASTER_FETCH_COMMAND_NUM=${MASTER_FETCH_COMMAND_NUM:-10} + +# Registry center configuration, determines the type and link of the registry center +export REGISTRY_TYPE=jdbc +export REGISTRY_HIKARI_CONFIG_JDBC_URL="jdbc:postgresql://postgres:5432/dolphinscheduler" +export REGISTRY_HIKARI_CONFIG_USERNAME=postgres +export REGISTRY_HIKARI_CONFIG_PASSWORD=postgres + +# Tasks related configurations, need to change the configuration if you use the related tasks. +export HADOOP_HOME=${HADOOP_HOME:-/opt/soft/hadoop} +export HADOOP_CONF_DIR=${HADOOP_CONF_DIR:-/opt/soft/hadoop/etc/hadoop} +export SPARK_HOME=${SPARK_HOME:-/opt/soft/spark} +export PYTHON_LAUNCHER=${PYTHON_LAUNCHER:-/opt/soft/python/bin/python3} +export HIVE_HOME=${HIVE_HOME:-/opt/soft/hive} +export FLINK_HOME=${FLINK_HOME:-/opt/soft/flink} +export DATAX_LAUNCHER=${DATAX_LAUNCHER:-/opt/soft/datax/bin/datax.py} + +export PATH=$HADOOP_HOME/bin:$SPARK_HOME/bin:$PYTHON_LAUNCHER:$JAVA_HOME/bin:$HIVE_HOME/bin:$FLINK_HOME/bin:$DATAX_LAUNCHER:$PATH + +export MASTER_RESERVED_MEMORY=0.01 +export WORKER_RESERVED_MEMORY=0.01 + +# applicationId auto collection related configuration, the following configurations are unnecessary if setting appId.collect=log +#export HADOOP_CLASSPATH=`hadoop classpath`:${DOLPHINSCHEDULER_HOME}/tools/libs/* +#export SPARK_DIST_CLASSPATH=$HADOOP_CLASSPATH:$SPARK_DIST_CLASS_PATH +#export HADOOP_CLIENT_OPTS="-javaagent:${DOLPHINSCHEDULER_HOME}/tools/libs/aspectjweaver-1.9.7.jar":$HADOOP_CLIENT_OPTS +#export SPARK_SUBMIT_OPTS="-javaagent:${DOLPHINSCHEDULER_HOME}/tools/libs/aspectjweaver-1.9.7.jar":$SPARK_SUBMIT_OPTS +#export FLINK_ENV_JAVA_OPTS="-javaagent:${DOLPHINSCHEDULER_HOME}/tools/libs/aspectjweaver-1.9.7.jar":$FLINK_ENV_JAVA_OPTS diff --git a/.github/workflows/cluster-test/postgresql_with_postgresql_registry/install_env.sh b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/install_env.sh new file mode 100644 index 000000000000..cd660febf88b --- /dev/null +++ b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/install_env.sh @@ -0,0 +1,58 @@ +# +# 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. +# + +# --------------------------------------------------------- +# INSTALL MACHINE +# --------------------------------------------------------- +# A comma separated list of machine hostname or IP would be installed DolphinScheduler, +# including master, worker, api, alert. If you want to deploy in pseudo-distributed +# mode, just write a pseudo-distributed hostname +# Example for hostnames: ips="ds1,ds2,ds3,ds4,ds5", Example for IPs: ips="192.168.8.1,192.168.8.2,192.168.8.3,192.168.8.4,192.168.8.5" +ips=${ips:-"localhost"} + +# Port of SSH protocol, default value is 22. For now we only support same port in all `ips` machine +# modify it if you use different ssh port +sshPort=${sshPort:-"22"} + +# A comma separated list of machine hostname or IP would be installed Master server, it +# must be a subset of configuration `ips`. +# Example for hostnames: masters="ds1,ds2", Example for IPs: masters="192.168.8.1,192.168.8.2" +masters=${masters:-"localhost"} + +# A comma separated list of machine : or :.All hostname or IP must be a +# subset of configuration `ips`, And workerGroup have default value as `default`, but we recommend you declare behind the hosts +# Example for hostnames: workers="ds1:default,ds2:default,ds3:default", Example for IPs: workers="192.168.8.1:default,192.168.8.2:default,192.168.8.3:default" +workers=${workers:-"localhost:default"} + +# A comma separated list of machine hostname or IP would be installed Alert server, it +# must be a subset of configuration `ips`. +# Example for hostname: alertServer="ds3", Example for IP: alertServer="192.168.8.3" +alertServer=${alertServer:-"localhost"} + +# A comma separated list of machine hostname or IP would be installed API server, it +# must be a subset of configuration `ips`. +# Example for hostname: apiServers="ds1", Example for IP: apiServers="192.168.8.1" +apiServers=${apiServers:-"localhost"} + +# The directory to install DolphinScheduler for all machine we config above. It will automatically be created by `install.sh` script if not exists. +# Do not set this configuration same as the current path (pwd) +installPath=${installPath:-"/root/apache-dolphinscheduler-*-SNAPSHOT-bin"} + +# The user to deploy DolphinScheduler for all machine we config above. For now user must create by yourself before running `install.sh` +# script. The user needs to have sudo privileges and permissions to operate hdfs. If hdfs is enabled than the root directory needs +# to be created by this user +deployUser=${deployUser:-"dolphinscheduler"} diff --git a/.github/workflows/cluster-test/postgresql/running_test.sh b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/running_test.sh similarity index 100% rename from .github/workflows/cluster-test/postgresql/running_test.sh rename to .github/workflows/cluster-test/postgresql_with_postgresql_registry/running_test.sh diff --git a/.github/workflows/cluster-test/postgresql_with_postgresql_registry/start-job.sh b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/start-job.sh new file mode 100644 index 000000000000..e2b6b630e8c8 --- /dev/null +++ b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/start-job.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# +# 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. +# +set -euox pipefail + +#Start base service containers +docker-compose -f .github/workflows/cluster-test/postgresql_with_postgresql_registry/docker-compose-base.yaml up -d + +#Build ds postgresql cluster image +docker build -t jdk8:ds_postgresql_cluster -f .github/workflows/cluster-test/postgresql_with_postgresql_registry/Dockerfile . + +#Start ds postgresql cluster container +docker-compose -f .github/workflows/cluster-test/postgresql_with_postgresql_registry/docker-compose-cluster.yaml up -d + +#Running tests +/bin/bash .github/workflows/cluster-test/postgresql_with_postgresql_registry/running_test.sh + +#Cleanup +docker rm -f $(docker ps -aq) diff --git a/.github/workflows/cluster-test/postgresql/Dockerfile b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/Dockerfile similarity index 77% rename from .github/workflows/cluster-test/postgresql/Dockerfile rename to .github/workflows/cluster-test/postgresql_with_zookeeper_registry/Dockerfile index 38234ee7b343..077b5c97b89f 100644 --- a/.github/workflows/cluster-test/postgresql/Dockerfile +++ b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/Dockerfile @@ -28,12 +28,12 @@ RUN mv /root/apache-dolphinscheduler-*-SNAPSHOT-bin /root/apache-dolphinschedule ENV DOLPHINSCHEDULER_HOME /root/apache-dolphinscheduler-test-SNAPSHOT-bin #Setting install.sh -COPY .github/workflows/cluster-test/postgresql/install_env.sh $DOLPHINSCHEDULER_HOME/bin/env/install_env.sh +COPY .github/workflows/cluster-test/postgresql_with_zookeeper_registry/install_env.sh $DOLPHINSCHEDULER_HOME/bin/env/install_env.sh #Setting dolphinscheduler_env.sh -COPY .github/workflows/cluster-test/postgresql/dolphinscheduler_env.sh $DOLPHINSCHEDULER_HOME/bin/env/dolphinscheduler_env.sh +COPY .github/workflows/cluster-test/postgresql_with_zookeeper_registry/dolphinscheduler_env.sh $DOLPHINSCHEDULER_HOME/bin/env/dolphinscheduler_env.sh #Deploy -COPY .github/workflows/cluster-test/postgresql/deploy.sh /root/deploy.sh +COPY .github/workflows/cluster-test/postgresql_with_zookeeper_registry/deploy.sh /root/deploy.sh CMD [ "/bin/bash", "/root/deploy.sh" ] diff --git a/.github/workflows/cluster-test/postgresql/deploy.sh b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/deploy.sh similarity index 100% rename from .github/workflows/cluster-test/postgresql/deploy.sh rename to .github/workflows/cluster-test/postgresql_with_zookeeper_registry/deploy.sh diff --git a/.github/workflows/cluster-test/postgresql/docker-compose-base.yaml b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/docker-compose-base.yaml similarity index 100% rename from .github/workflows/cluster-test/postgresql/docker-compose-base.yaml rename to .github/workflows/cluster-test/postgresql_with_zookeeper_registry/docker-compose-base.yaml diff --git a/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/docker-compose-cluster.yaml b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/docker-compose-cluster.yaml new file mode 100644 index 000000000000..9ab79ea44dee --- /dev/null +++ b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/docker-compose-cluster.yaml @@ -0,0 +1,29 @@ +# +# 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" + +services: + ds: + container_name: ds + image: jdk8:ds_postgresql_cluster + restart: always + ports: + - "12345:12345" + - "5679:5679" + - "1235:1235" + - "50053:50053" diff --git a/.github/workflows/cluster-test/postgresql/dolphinscheduler_env.sh b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/dolphinscheduler_env.sh similarity index 100% rename from .github/workflows/cluster-test/postgresql/dolphinscheduler_env.sh rename to .github/workflows/cluster-test/postgresql_with_zookeeper_registry/dolphinscheduler_env.sh diff --git a/.github/workflows/cluster-test/postgresql/install_env.sh b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/install_env.sh similarity index 100% rename from .github/workflows/cluster-test/postgresql/install_env.sh rename to .github/workflows/cluster-test/postgresql_with_zookeeper_registry/install_env.sh diff --git a/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/running_test.sh b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/running_test.sh new file mode 100644 index 000000000000..0bc861c389d1 --- /dev/null +++ b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/running_test.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# +# 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. +# +set -x + + +API_HEALTHCHECK_COMMAND="curl -I -m 10 -o /dev/null -s -w %{http_code} http://0.0.0.0:12345/dolphinscheduler/actuator/health" +MASTER_HEALTHCHECK_COMMAND="curl -I -m 10 -o /dev/null -s -w %{http_code} http://0.0.0.0:5679/actuator/health" +WORKER_HEALTHCHECK_COMMAND="curl -I -m 10 -o /dev/null -s -w %{http_code} http://0.0.0.0:1235/actuator/health" +ALERT_HEALTHCHECK_COMMAND="curl -I -m 10 -o /dev/null -s -w %{http_code} http://0.0.0.0:50053/actuator/health" + +#Cluster start health check +TIMEOUT=180 +START_HEALTHCHECK_EXITCODE=0 + +for ((i=1; i<=TIMEOUT; i++)) +do + MASTER_HTTP_STATUS=$(eval "$MASTER_HEALTHCHECK_COMMAND") + WORKER_HTTP_STATUS=$(eval "$WORKER_HEALTHCHECK_COMMAND") + API_HTTP_STATUS=$(eval "$API_HEALTHCHECK_COMMAND") + ALERT_HTTP_STATUS=$(eval "$ALERT_HEALTHCHECK_COMMAND") + if [[ $MASTER_HTTP_STATUS -eq 200 && $WORKER_HTTP_STATUS -eq 200 && $API_HTTP_STATUS -eq 200 && $ALERT_HTTP_STATUS -eq 200 ]];then + START_HEALTHCHECK_EXITCODE=0 + else + START_HEALTHCHECK_EXITCODE=2 + fi + + if [[ $START_HEALTHCHECK_EXITCODE -eq 0 ]];then + echo "cluster start health check success" + break + fi + + if [[ $i -eq $TIMEOUT ]];then + if [[ $MASTER_HTTP_STATUS -ne 200 ]];then + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/master-server/logs/dolphinscheduler-master.log" + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/master-server/logs/*.out" + echo "master start health check failed" + fi + if [[ $WORKER_HTTP_STATUS -ne 200 ]]; then + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/worker-server/logs/dolphinscheduler-worker.log" + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/worker-server/logs/*.out" + echo "worker start health check failed" + fi + if [[ $API_HTTP_STATUS -ne 200 ]]; then + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/api-server/logs/dolphinscheduler-api.log" + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/api-server/logs/*.out" + echo "api start health check failed" + fi + if [[ $ALERT_HTTP_STATUS -ne 200 ]]; then + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/alert-server/logs/dolphinscheduler-alert.log" + docker exec -u root ds bash -c "cat /root/apache-dolphinscheduler-*-SNAPSHOT-bin/alert-server/logs/*.out" + echo "alert start health check failed" + fi + exit $START_HEALTHCHECK_EXITCODE + fi + + sleep 1 +done + +#Stop Cluster +docker exec -u root ds bash -c "/root/apache-dolphinscheduler-*-SNAPSHOT-bin/bin/stop-all.sh" + +#Cluster stop health check +sleep 5 +MASTER_HTTP_STATUS=$(eval "$MASTER_HEALTHCHECK_COMMAND") +if [[ $MASTER_HTTP_STATUS -ne 200 ]];then + echo "master stop health check success" +else + echo "master stop health check failed" + exit 3 +fi + +WORKER_HTTP_STATUS=$(eval "$WORKER_HEALTHCHECK_COMMAND") +if [[ $WORKER_HTTP_STATUS -ne 200 ]];then + echo "worker stop health check success" +else + echo "worker stop health check failed" + exit 3 +fi + +API_HTTP_STATUS=$(eval "$API_HEALTHCHECK_COMMAND") +if [[ $API_HTTP_STATUS -ne 200 ]];then + echo "api stop health check success" +else + echo "api stop health check failed" + exit 3 +fi + +ALERT_HTTP_STATUS=$(eval "$ALERT_HEALTHCHECK_COMMAND") +if [[ $ALERT_HTTP_STATUS -ne 200 ]];then + echo "alert stop health check success" +else + echo "alert stop health check failed" + exit 3 +fi diff --git a/.github/workflows/cluster-test/postgresql/start-job.sh b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/start-job.sh similarity index 73% rename from .github/workflows/cluster-test/postgresql/start-job.sh rename to .github/workflows/cluster-test/postgresql_with_zookeeper_registry/start-job.sh index ba0878e3ecf2..fe755c97f1a8 100644 --- a/.github/workflows/cluster-test/postgresql/start-job.sh +++ b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/start-job.sh @@ -18,16 +18,16 @@ set -euox pipefail #Start base service containers -docker-compose -f .github/workflows/cluster-test/postgresql/docker-compose-base.yaml up -d +docker-compose -f .github/workflows/cluster-test/postgresql_with_zookeeper_registry/docker-compose-base.yaml up -d #Build ds postgresql cluster image -docker build -t jdk8:ds_postgresql_cluster -f .github/workflows/cluster-test/postgresql/Dockerfile . +docker build -t jdk8:ds_postgresql_cluster -f .github/workflows/cluster-test/postgresql_with_zookeeper_registry/Dockerfile . #Start ds postgresql cluster container -docker-compose -f .github/workflows/cluster-test/postgresql/docker-compose-cluster.yaml up -d +docker-compose -f .github/workflows/cluster-test/postgresql_with_zookeeper_registry/docker-compose-cluster.yaml up -d #Running tests -/bin/bash .github/workflows/cluster-test/postgresql/running_test.sh +/bin/bash .github/workflows/cluster-test/postgresql_with_zookeeper_registry/running_test.sh #Cleanup docker rm -f $(docker ps -aq) diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java index fd3d4b02e34a..ff0088ea8d30 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java @@ -27,21 +27,24 @@ import org.apache.dolphinscheduler.common.lifecycle.ServerLifeCycleManager; import org.apache.dolphinscheduler.common.thread.DefaultUncaughtExceptionHandler; import org.apache.dolphinscheduler.common.thread.ThreadUtils; +import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration; +import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.event.EventListener; +import org.springframework.context.annotation.FilterType; -@SpringBootApplication -@ComponentScan("org.apache.dolphinscheduler") @Slf4j +@SpringBootApplication +@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = JdbcRegistryAutoConfiguration.class) +}) public class AlertServer { @Autowired @@ -59,11 +62,11 @@ public static void main(String[] args) { AlertServerMetrics.registerUncachedException(DefaultUncaughtExceptionHandler::getUncaughtExceptionCount); Thread.setDefaultUncaughtExceptionHandler(DefaultUncaughtExceptionHandler.getInstance()); Thread.currentThread().setName(Constants.THREAD_NAME_ALERT_SERVER); - new SpringApplicationBuilder(AlertServer.class).run(args); + SpringApplication.run(AlertServer.class, args); } - @EventListener - public void run(ApplicationReadyEvent readyEvent) { + @PostConstruct + public void run() { log.info("Alert server is staring ..."); alertPluginManager.start(); alertRegistryClient.start(); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/ApiApplicationServer.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/ApiApplicationServer.java index c7e6d9778f60..20a641207694 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/ApiApplicationServer.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/ApiApplicationServer.java @@ -22,6 +22,7 @@ import org.apache.dolphinscheduler.common.thread.DefaultUncaughtExceptionHandler; import org.apache.dolphinscheduler.dao.PluginDao; import org.apache.dolphinscheduler.dao.entity.PluginDefine; +import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration; import org.apache.dolphinscheduler.plugin.task.api.TaskChannelFactory; import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.spi.params.PluginParamsTransfer; @@ -38,11 +39,14 @@ import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; import org.springframework.context.event.EventListener; @ServletComponentScan @SpringBootApplication -@ComponentScan("org.apache.dolphinscheduler") +@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = JdbcRegistryAutoConfiguration.class) +}) @Slf4j public class ApiApplicationServer { diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/PluginDao.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/PluginDao.java index 71e3be70c452..24cb022881f6 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/PluginDao.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/PluginDao.java @@ -29,10 +29,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; @Slf4j -@Component +@Repository public class PluginDao { @Autowired diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/AlertDaoTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java similarity index 95% rename from dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/AlertDaoTest.java rename to dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java index f2cb503d9bfe..c0a841c1a063 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/AlertDaoTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java @@ -15,10 +15,12 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.dao; +package org.apache.dolphinscheduler.dao.repository.impl; import org.apache.dolphinscheduler.common.enums.AlertStatus; import org.apache.dolphinscheduler.common.enums.ProfileType; +import org.apache.dolphinscheduler.dao.AlertDao; +import org.apache.dolphinscheduler.dao.DaoConfiguration; import org.apache.dolphinscheduler.dao.entity.Alert; import java.util.List; diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java index 752479e60086..fd262a8c6e06 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java @@ -24,6 +24,7 @@ import org.apache.dolphinscheduler.common.thread.ThreadUtils; import org.apache.dolphinscheduler.meter.metrics.MetricsProvider; import org.apache.dolphinscheduler.meter.metrics.SystemMetrics; +import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration; import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.scheduler.api.SchedulerApi; import org.apache.dolphinscheduler.server.master.metrics.MasterServerMetrics; @@ -46,10 +47,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication -@ComponentScan("org.apache.dolphinscheduler") +@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = JdbcRegistryAutoConfiguration.class) +}) @EnableTransactionManagement @EnableCaching @Slf4j diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/README.md b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/README.md index 3b1a2cb24f76..554c375218e8 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/README.md +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/README.md @@ -1,6 +1,7 @@ # Introduction -This module is the jdbc registry plugin module, this plugin will use jdbc as the registry center. Will use the database configuration same as DolphinScheduler in api'yaml default. +This module is the jdbc registry plugin module, this plugin will use jdbc as the registry center. Will use the database +configuration same as DolphinScheduler in api'yaml default. # How to use @@ -22,8 +23,11 @@ registry: After do this two steps, you can start your DolphinScheduler cluster, your cluster will use mysql as registry center to store server metadata. -NOTE: You need to add `mysql-connector-java.jar` into DS classpath if you use mysql database, since this plugin will not bundle this driver in distribution. -You can get the detail about Initialize the Database. +NOTE: You need to add `mysql-connector-java.jar` into DS classpath if you use mysql database, since this plugin will not +bundle this driver in distribution. +You can get the detail +about Initialize the +Database. ## Optional configuration diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/pom.xml b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/pom.xml index 47b644929364..d4285edfbdf4 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/pom.xml +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/pom.xml @@ -62,6 +62,16 @@ mybatis-plus + + com.baomidou + mybatis-plus-boot-starter + + + org.apache.logging.log4j + log4j-to-slf4j + + + diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryConfiguration.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java similarity index 65% rename from dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryConfiguration.java rename to dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java index 7b37749ab77f..09211f99fbbf 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryConfiguration.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java @@ -22,40 +22,56 @@ import org.apache.ibatis.session.SqlSessionFactory; +import lombok.extern.slf4j.Slf4j; + import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import com.zaxxer.hikari.HikariDataSource; -@Configuration +@Slf4j +@Configuration(proxyBeanMethods = false) +@MapperScan("org.apache.dolphinscheduler.plugin.registry.jdbc.mapper") @ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "jdbc") -public class JdbcRegistryConfiguration { +@AutoConfigureAfter(MybatisPlusAutoConfiguration.class) +public class JdbcRegistryAutoConfiguration { + + public JdbcRegistryAutoConfiguration() { + log.info("Load JdbcRegistryAutoConfiguration"); + } @Bean - @ConditionalOnProperty(prefix = "registry.hikari-config", name = "jdbc-url") - public SqlSessionFactory jdbcRegistrySqlSessionFactory(JdbcRegistryProperties jdbcRegistryProperties) throws Exception { + @ConditionalOnMissingBean + public SqlSessionFactory sqlSessionFactory(JdbcRegistryProperties jdbcRegistryProperties) throws Exception { + log.info("Initialize jdbcRegistrySqlSessionFactory"); MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(new HikariDataSource(jdbcRegistryProperties.getHikariConfig())); return sqlSessionFactoryBean.getObject(); } @Bean - public SqlSessionTemplate jdbcRegistrySqlSessionTemplate(SqlSessionFactory jdbcRegistrySqlSessionFactory) { - jdbcRegistrySqlSessionFactory.getConfiguration().addMapper(JdbcRegistryDataMapper.class); - jdbcRegistrySqlSessionFactory.getConfiguration().addMapper(JdbcRegistryLockMapper.class); + @ConditionalOnMissingBean + public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory jdbcRegistrySqlSessionFactory) { + log.info("Initialize jdbcRegistrySqlSessionTemplate"); return new SqlSessionTemplate(jdbcRegistrySqlSessionFactory); } @Bean public JdbcRegistryDataMapper jdbcRegistryDataMapper(SqlSessionTemplate jdbcRegistrySqlSessionTemplate) { + jdbcRegistrySqlSessionTemplate.getConfiguration().addMapper(JdbcRegistryDataMapper.class); return jdbcRegistrySqlSessionTemplate.getMapper(JdbcRegistryDataMapper.class); } @Bean public JdbcRegistryLockMapper jdbcRegistryLockMapper(SqlSessionTemplate jdbcRegistrySqlSessionTemplate) { + jdbcRegistrySqlSessionTemplate.getConfiguration().addMapper(JdbcRegistryLockMapper.class); return jdbcRegistrySqlSessionTemplate.getMapper(JdbcRegistryLockMapper.class); } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/resources/META-INF/spring.factories b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..aabe7e325203 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/resources/mysql_registry_init.sql b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/resources/mysql_registry_init.sql index 30af3066ff53..6df206b3912c 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/resources/mysql_registry_init.sql +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/resources/mysql_registry_init.sql @@ -15,7 +15,6 @@ * limitations under the License. */ -SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS `t_ds_jdbc_registry_data`; CREATE TABLE `t_ds_jdbc_registry_data` diff --git a/dolphinscheduler-tools/src/main/bin/initialize-jdbc-registry.sh b/dolphinscheduler-tools/src/main/bin/initialize-jdbc-registry.sh new file mode 100644 index 000000000000..895d62a0bbaa --- /dev/null +++ b/dolphinscheduler-tools/src/main/bin/initialize-jdbc-registry.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# 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. +# + +BIN_DIR=$(dirname $0) +DOLPHINSCHEDULER_HOME=${DOLPHINSCHEDULER_HOME:-$(cd $BIN_DIR/../..; pwd)} + +if [ "$DOCKER" != "true" ]; then + source "$DOLPHINSCHEDULER_HOME/bin/env/dolphinscheduler_env.sh" +fi + +JAVA_OPTS=${JAVA_OPTS:-"-server -Duser.timezone=${SPRING_JACKSON_TIME_ZONE} -Xms1g -Xmx1g -Xmn512m -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.hprof"} + +$JAVA_HOME/bin/java $JAVA_OPTS \ + -cp "$DOLPHINSCHEDULER_HOME/tools/conf":"$DOLPHINSCHEDULER_HOME/tools/libs/*":"$DOLPHINSCHEDULER_HOME/tools/sql" \ + -Dspring.profiles.active=${DATABASE} \ + org.apache.dolphinscheduler.tools.command.CommandApplication diff --git a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/command/CommandApplication.java b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/command/CommandApplication.java new file mode 100644 index 000000000000..1e419f4d19de --- /dev/null +++ b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/command/CommandApplication.java @@ -0,0 +1,152 @@ +/* + * 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.tools.command; + +import org.apache.dolphinscheduler.dao.DaoConfiguration; +import org.apache.dolphinscheduler.dao.plugin.api.dialect.DatabaseDialect; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.stereotype.Component; + +import com.baomidou.mybatisplus.annotation.DbType; + +// todo: use spring-shell to manage the command +@SpringBootApplication +@ImportAutoConfiguration(DaoConfiguration.class) +public class CommandApplication { + + public static void main(String[] args) { + SpringApplication.run(CommandApplication.class, args); + } + + @Component + @Slf4j + static class JdbcRegistrySchemaInitializeCommand implements CommandLineRunner { + + @Autowired + private DatabaseDialect databaseDialect; + + @Autowired + private DbType dbType; + + @Autowired + private DataSource dataSource; + + JdbcRegistrySchemaInitializeCommand() { + } + + @Override + public void run(String... args) throws Exception { + if (databaseDialect.tableExists("t_ds_jdbc_registry_data") + || databaseDialect.tableExists("t_ds_jdbc_registry_lock")) { + log.warn("t_ds_jdbc_registry_data/t_ds_jdbc_registry_lock already exists"); + return; + } + if (dbType == DbType.MYSQL) { + jdbcRegistrySchemaInitializeInMysql(); + } else if (dbType == DbType.POSTGRE_SQL) { + jdbcRegistrySchemaInitializeInPG(); + } else { + log.error("Unsupported database type: {}", dbType); + } + } + + private void jdbcRegistrySchemaInitializeInMysql() throws SQLException { + try ( + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE `t_ds_jdbc_registry_data`\n" + + "(\n" + + " `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',\n" + + " `data_key` varchar(256) NOT NULL COMMENT 'key, like zookeeper node path',\n" + + " `data_value` text NOT NULL COMMENT 'data, like zookeeper node value',\n" + + " `data_type` tinyint(4) NOT NULL COMMENT '1: ephemeral node, 2: persistent node',\n" + + + " `last_term` bigint NOT NULL COMMENT 'last term time',\n" + + " `last_update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'last update time',\n" + + + " `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',\n" + + + " PRIMARY KEY (`id`),\n" + + " unique (`data_key`)\n" + + ") ENGINE = InnoDB\n" + + " DEFAULT CHARSET = utf8;"); + + statement.execute("CREATE TABLE `t_ds_jdbc_registry_lock`\n" + + "(\n" + + " `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',\n" + + " `lock_key` varchar(256) NOT NULL COMMENT 'lock path',\n" + + " `lock_owner` varchar(256) NOT NULL COMMENT 'the lock owner, ip_processId',\n" + + " `last_term` bigint NOT NULL COMMENT 'last term time',\n" + + " `last_update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'last update time',\n" + + + " `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',\n" + + + " PRIMARY KEY (`id`),\n" + + " unique (`lock_key`)\n" + + ") ENGINE = InnoDB\n" + + " DEFAULT CHARSET = utf8;"); + } + } + + private void jdbcRegistrySchemaInitializeInPG() throws SQLException { + try ( + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.execute("create table t_ds_jdbc_registry_data\n" + + "(\n" + + " id serial\n" + + " constraint t_ds_jdbc_registry_data_pk primary key,\n" + + " data_key varchar not null,\n" + + " data_value text not null,\n" + + " data_type int4 not null,\n" + + " last_term bigint not null,\n" + + " last_update_time timestamp default current_timestamp not null,\n" + + " create_time timestamp default current_timestamp not null\n" + + ");"); + statement.execute( + "create unique index t_ds_jdbc_registry_data_key_uindex on t_ds_jdbc_registry_data (data_key);"); + statement.execute("create table t_ds_jdbc_registry_lock\n" + + "(\n" + + " id serial\n" + + " constraint t_ds_jdbc_registry_lock_pk primary key,\n" + + " lock_key varchar not null,\n" + + " lock_owner varchar not null,\n" + + " last_term bigint not null,\n" + + " last_update_time timestamp default current_timestamp not null,\n" + + " create_time timestamp default current_timestamp not null\n" + + ");"); + statement.execute( + "create unique index t_ds_jdbc_registry_lock_key_uindex on t_ds_jdbc_registry_lock (lock_key);"); + } + } + + } +} diff --git a/dolphinscheduler-tools/src/main/resources/application.yaml b/dolphinscheduler-tools/src/main/resources/application.yaml index 38752021dc9e..136a4c5fd4c4 100644 --- a/dolphinscheduler-tools/src/main/resources/application.yaml +++ b/dolphinscheduler-tools/src/main/resources/application.yaml @@ -63,6 +63,8 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/dolphinscheduler?useUnicode=true&characterEncoding=UTF-8 + username: root + password: root --- spring: diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java index 86e755fc8a24..b0866f7fd8ff 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java @@ -24,6 +24,7 @@ import org.apache.dolphinscheduler.common.thread.ThreadUtils; import org.apache.dolphinscheduler.meter.metrics.MetricsProvider; import org.apache.dolphinscheduler.meter.metrics.SystemMetrics; +import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration; import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.plugin.task.api.utils.LogUtils; @@ -47,11 +48,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @EnableTransactionManagement -@ComponentScan(basePackages = "org.apache.dolphinscheduler") +@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = JdbcRegistryAutoConfiguration.class) +}) @Slf4j public class WorkerServer implements IStoppable { From 8fc204940f7be5cf356f63dd223bc599f857fe69 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 18 Apr 2024 16:50:50 +0800 Subject: [PATCH 038/165] Add DSIP template (#15871) --- .github/ISSUE_TEMPLATE/dsip-request.yml | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/dsip-request.yml diff --git a/.github/ISSUE_TEMPLATE/dsip-request.yml b/.github/ISSUE_TEMPLATE/dsip-request.yml new file mode 100644 index 000000000000..0908bba9e769 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/dsip-request.yml @@ -0,0 +1,77 @@ +# +# 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. +# +name: Feature request +description: Suggest an idea for this project +title: "[DSIP-][Module Name] DSIP title" +labels: [ "DSIP", "Waiting for reply" ] +body: + - type: markdown + attributes: + value: | + For better global communication, Please write in English. + + If you feel the description in English is not clear, then you can append description in Chinese, thanks! + + - type: checkboxes + attributes: + label: Search before asking + description: > + Please make sure to search in the [DSIP](https://github.com/apache/dolphinscheduler/issues/14102) first + to see whether the same DSIP was created already. + options: + - label: > + I had searched in the [DSIP](https://github.com/apache/dolphinscheduler/issues/14102) and found no + similar DSIP. + required: true + + - type: textarea + attributes: + label: Motivation + description: Why you want to do this change? + + - type: textarea + attributes: + label: Design Detail + description: Your design. + placeholder: > + It's better to provide a detailed design, such as the design of the interface, the design of the database, etc. + + - type: textarea + attributes: + label: Compatibility, Deprecation, and Migration Plan + description: > + If this feature is related to compatibility, deprecation, or migration, please describe it here. + + - type: textarea + attributes: + label: Test Plan + description: > + How to test this improvement. + + - type: checkboxes + attributes: + label: Code of Conduct + description: | + The Code of Conduct helps create a safe space for everyone. We require that everyone agrees to it. + options: + - label: | + I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct) + required: true + + - type: markdown + attributes: + value: "Thanks for completing our form!" From 325bfa821f9cd82ee15b681e8a5ae30d4976f8f1 Mon Sep 17 00:00:00 2001 From: Evan Sun Date: Thu, 18 Apr 2024 20:40:05 +0800 Subject: [PATCH 039/165] [TEST] increase cov of logger service (#15870) Co-authored-by: abzymeinsjtu Co-authored-by: Eric Gao --- .../api/service/impl/LoggerServiceImpl.java | 3 +- .../api/service/LoggerServiceTest.java | 62 ++++++++++++++++++- .../api/utils/ServiceTestUtil.java | 18 ++++++ 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/LoggerServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/LoggerServiceImpl.java index 0663b883747e..c0ecb0e9b58a 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/LoggerServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/LoggerServiceImpl.java @@ -237,7 +237,7 @@ private byte[] getLogBytes(TaskInstance taskInstance) { host, Constants.SYSTEM_LINE_SEPARATOR).getBytes(StandardCharsets.UTF_8); - byte[] logBytes = new byte[0]; + byte[] logBytes; ILogService iLogService = SingletonJdkDynamicRpcClientProxyFactory.getProxyClient(taskInstance.getHost(), ILogService.class); @@ -251,6 +251,5 @@ private byte[] getLogBytes(TaskInstance taskInstance) { log.error("Download TaskInstance: {} Log Error", taskInstance.getName(), ex); throw new ServiceException(Status.DOWNLOAD_TASK_INSTANCE_LOG_FILE_ERROR); } - } } diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/LoggerServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/LoggerServiceTest.java index 2c4de2ab7ef4..4861e1004e4b 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/LoggerServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/LoggerServiceTest.java @@ -18,8 +18,10 @@ package org.apache.dolphinscheduler.api.service; import static org.apache.dolphinscheduler.api.AssertionsHelper.assertDoesNotThrow; +import static org.apache.dolphinscheduler.api.AssertionsHelper.assertThrowsServiceException; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.DOWNLOAD_LOG; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.VIEW_LOG; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -109,11 +111,25 @@ public void setUp() { @Override public TaskInstanceLogFileDownloadResponse getTaskInstanceWholeLogFileBytes(TaskInstanceLogFileDownloadRequest taskInstanceLogFileDownloadRequest) { - return new TaskInstanceLogFileDownloadResponse(new byte[0]); + if (taskInstanceLogFileDownloadRequest.getTaskInstanceId() == 1) { + return new TaskInstanceLogFileDownloadResponse(new byte[0]); + } else if (taskInstanceLogFileDownloadRequest.getTaskInstanceId() == 10) { + return new TaskInstanceLogFileDownloadResponse("log content".getBytes()); + } + + throw new ServiceException("download error"); } @Override public TaskInstanceLogPageQueryResponse pageQueryTaskInstanceLog(TaskInstanceLogPageQueryRequest taskInstanceLogPageQueryRequest) { + if (taskInstanceLogPageQueryRequest.getTaskInstanceId() != null) { + if (taskInstanceLogPageQueryRequest.getTaskInstanceId() == 100) { + throw new ServiceException("query log error"); + } else if (taskInstanceLogPageQueryRequest.getTaskInstanceId() == 10) { + return new TaskInstanceLogPageQueryResponse("log content"); + } + } + return new TaskInstanceLogPageQueryResponse(); } @@ -177,6 +193,13 @@ public void testQueryLog() { when(taskInstanceDao.queryById(1)).thenReturn(taskInstance); result = loggerService.queryLog(loginUser, 1, 1, 1); Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode().intValue()); + + result = loggerService.queryLog(loginUser, 1, 0, 1); + Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode().intValue()); + + taskInstance.setLogPath(""); + assertThrowsServiceException(Status.QUERY_TASK_INSTANCE_LOG_ERROR, + () -> loggerService.queryLog(loginUser, 1, 1, 1)); } @Test @@ -237,9 +260,15 @@ public void testQueryLogInSpecifiedProject() { loginUser.setUserType(UserType.GENERAL_USER); TaskInstance taskInstance = new TaskInstance(); when(taskInstanceDao.queryById(1)).thenReturn(taskInstance); + when(taskInstanceDao.queryById(10)).thenReturn(null); + + assertThrowsServiceException(Status.TASK_INSTANCE_NOT_FOUND, + () -> loggerService.queryLog(loginUser, projectCode, 10, 1, 1)); + TaskDefinition taskDefinition = new TaskDefinition(); taskDefinition.setProjectCode(projectCode); taskDefinition.setCode(1L); + // SUCCESS taskInstance.setTaskCode(1L); taskInstance.setId(1); @@ -249,13 +278,27 @@ public void testQueryLogInSpecifiedProject() { when(taskInstanceDao.queryById(1)).thenReturn(taskInstance); when(taskDefinitionMapper.queryByCode(taskInstance.getTaskCode())).thenReturn(taskDefinition); assertDoesNotThrow(() -> loggerService.queryLog(loginUser, projectCode, 1, 1, 1)); + + taskDefinition.setProjectCode(10); + assertThrowsServiceException(Status.TASK_INSTANCE_NOT_FOUND, + () -> loggerService.queryLog(loginUser, projectCode, 1, 1, 1)); + + taskDefinition.setProjectCode(1); + taskInstance.setId(10); + when(taskInstanceDao.queryById(10)).thenReturn(taskInstance); + String result = loggerService.queryLog(loginUser, projectCode, 10, 1, 1); + assertEquals("log content", result); + + taskInstance.setId(100); + when(taskInstanceDao.queryById(100)).thenReturn(taskInstance); + assertThrowsServiceException(Status.QUERY_TASK_INSTANCE_LOG_ERROR, + () -> loggerService.queryLog(loginUser, projectCode, 10, 1, 1)); } @Test public void testGetLogBytesInSpecifiedProject() { long projectCode = 1L; when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); - Project project = getProject(projectCode); User loginUser = new User(); loginUser.setId(-1); @@ -272,9 +315,24 @@ public void testGetLogBytesInSpecifiedProject() { taskInstance.setHost("127.0.0.1:" + nettyServerPort); taskInstance.setLogPath("/temp/log"); doNothing().when(projectService).checkProjectAndAuthThrowException(loginUser, projectCode, DOWNLOAD_LOG); + + when(taskInstanceDao.queryById(1)).thenReturn(null); + assertThrowsServiceException( + Status.INTERNAL_SERVER_ERROR_ARGS, () -> loggerService.getLogBytes(loginUser, projectCode, 1)); + when(taskInstanceDao.queryById(1)).thenReturn(taskInstance); when(taskDefinitionMapper.queryByCode(taskInstance.getTaskCode())).thenReturn(taskDefinition); assertDoesNotThrow(() -> loggerService.getLogBytes(loginUser, projectCode, 1)); + + taskDefinition.setProjectCode(2L); + assertThrowsServiceException(Status.INTERNAL_SERVER_ERROR_ARGS, + () -> loggerService.getLogBytes(loginUser, projectCode, 1)); + + taskDefinition.setProjectCode(1L); + taskInstance.setId(100); + when(taskInstanceDao.queryById(100)).thenReturn(taskInstance); + assertThrowsServiceException(Status.DOWNLOAD_TASK_INSTANCE_LOG_FILE_ERROR, + () -> loggerService.getLogBytes(loginUser, projectCode, 100)); } /** diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/ServiceTestUtil.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/ServiceTestUtil.java index 0dc71841b439..f33571d30909 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/ServiceTestUtil.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/utils/ServiceTestUtil.java @@ -17,6 +17,9 @@ package org.apache.dolphinscheduler.api.utils; +import org.apache.dolphinscheduler.common.enums.UserType; +import org.apache.dolphinscheduler.dao.entity.User; + import java.nio.charset.StandardCharsets; import java.util.Random; @@ -27,4 +30,19 @@ public static String randomStringWithLengthN(int n) { new Random().nextBytes(bitArray); return new String(bitArray, StandardCharsets.UTF_8); } + + private static User getUser(Integer userId, String userName, UserType userType) { + User user = new User(); + user.setUserType(userType); + user.setId(userId); + user.setUserName(userName); + return user; + } + + public static User getAdminUser() { + return getUser(1, "admin", UserType.ADMIN_USER); + } + public static User getGeneralUser() { + return getUser(10, "user", UserType.GENERAL_USER); + } } From 32f92889746ccff0ee0379fd1b17ebe49a0591a7 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Fri, 19 Apr 2024 10:31:34 +0800 Subject: [PATCH 040/165] Fix dsip name (#15876) --- .github/ISSUE_TEMPLATE/dsip-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/dsip-request.yml b/.github/ISSUE_TEMPLATE/dsip-request.yml index 0908bba9e769..f54421b06624 100644 --- a/.github/ISSUE_TEMPLATE/dsip-request.yml +++ b/.github/ISSUE_TEMPLATE/dsip-request.yml @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -name: Feature request +name: DSIP description: Suggest an idea for this project title: "[DSIP-][Module Name] DSIP title" labels: [ "DSIP", "Waiting for reply" ] From d306f1d04b3b4259093d139a28b2ad4d55d1dcf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=BA=E9=98=B3?= Date: Fri, 19 Apr 2024 15:20:31 +0800 Subject: [PATCH 041/165] Refactor record audit log logic (#15881) --- .../api/audit/OperatorLogAspect.java | 74 +++++++++++++++---- .../api/audit/OperatorUtils.java | 25 ++----- .../api/audit/operator/AuditOperator.java | 16 ++-- .../api/audit/operator/BaseAuditOperator.java | 41 +++++----- .../impl/TaskDefinitionServiceImpl.java | 6 +- .../common/constants/Constants.java | 4 + .../common/enums/AuditModelType.java | 2 +- 7 files changed, 99 insertions(+), 69 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorLogAspect.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorLogAspect.java index da8a7bd3e666..aaf35d5b6662 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorLogAspect.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorLogAspect.java @@ -17,18 +17,26 @@ package org.apache.dolphinscheduler.api.audit; +import org.apache.dolphinscheduler.api.audit.enums.AuditType; import org.apache.dolphinscheduler.api.audit.operator.AuditOperator; -import org.apache.dolphinscheduler.api.utils.Result; +import org.apache.dolphinscheduler.dao.entity.AuditLog; +import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.service.bean.SpringApplicationContext; import java.lang.reflect.Method; +import java.util.List; import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; @@ -40,34 +48,74 @@ @Component public class OperatorLogAspect { + private static final ThreadLocal auditThreadLocal = new ThreadLocal<>(); + @Pointcut("@annotation(org.apache.dolphinscheduler.api.audit.OperatorLog)") public void logPointCut() { } - @Around("logPointCut()") - public Object around(ProceedingJoinPoint point) throws Throwable { + @Before("logPointCut()") + public void before(JoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); - OperatorLog operatorLog = method.getAnnotation(OperatorLog.class); - Operation operation = method.getAnnotation(Operation.class); + if (operation == null) { log.warn("Operation is null of method: {}", method.getName()); - return point.proceed(); + return; } - long beginTime = System.currentTimeMillis(); Map paramsMap = OperatorUtils.getParamsMap(point, signature); - Result result = (Result) point.proceed(); + User user = OperatorUtils.getUser(paramsMap); + if (user == null) { + log.error("user is null"); + return; + } + + AuditType auditType = operatorLog.auditType(); + try { AuditOperator operator = SpringApplicationContext.getBean(operatorLog.auditType().getOperatorClass()); - long latency = System.currentTimeMillis() - beginTime; - operator.recordAudit(paramsMap, result, latency, operation, operatorLog); + List auditLogList = OperatorUtils.buildAuditLogList(operation.description(), auditType, user); + operator.setRequestParam(auditType, auditLogList, paramsMap); + AuditContext auditContext = + new AuditContext(auditLogList, paramsMap, operatorLog, System.currentTimeMillis(), operator); + auditThreadLocal.set(auditContext); } catch (Throwable throwable) { log.error("Record audit log error", throwable); } + } + + @AfterReturning(value = "logPointCut()", returning = "returnValue") + public void afterReturning(Object returnValue) { + try { + AuditContext auditContext = auditThreadLocal.get(); + if (auditContext == null) { + return; + } + auditContext.getOperator().recordAudit(auditContext, returnValue); + } catch (Throwable throwable) { + log.error("Record audit log error", throwable); + } finally { + auditThreadLocal.remove(); + } + } + + @AfterThrowing("logPointCut()") + public void afterThrowing() { + auditThreadLocal.remove(); + } + + @Getter + @Setter + @AllArgsConstructor + public static class AuditContext { - return result; + List auditLogList; + Map paramsMap; + OperatorLog operatorLog; + long beginTime; + AuditOperator operator; } } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java index 10ccf8d64824..8dc628b57602 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/OperatorUtils.java @@ -20,6 +20,7 @@ import org.apache.dolphinscheduler.api.audit.enums.AuditType; import org.apache.dolphinscheduler.api.enums.ExecuteType; import org.apache.dolphinscheduler.api.utils.Result; +import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.enums.AuditModelType; import org.apache.dolphinscheduler.common.enums.AuditOperationType; import org.apache.dolphinscheduler.common.enums.ReleaseState; @@ -36,24 +37,12 @@ import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; @Slf4j public class OperatorUtils { - protected void changeObjectForVersionRelated(AuditOperationType auditOperationType, Map paramsMap, - List auditLogList) { - switch (auditOperationType) { - case SWITCH_VERSION: - case DELETE_VERSION: - auditLogList.get(0).setModelName(paramsMap.get("version").toString()); - break; - default: - break; - } - } - public static boolean resultFail(Result result) { return result != null && result.isFailed(); } @@ -66,7 +55,7 @@ public static List buildAuditLogList(String apiDescription, AuditType auditLog.setOperationType(auditType.getAuditOperationType().getName()); auditLog.setDescription(apiDescription); auditLog.setCreateTime(new Date()); - + auditLogList.add(auditLog); return auditLogList; } @@ -80,7 +69,7 @@ public static User getUser(Map paramsMap) { return null; } - public static Map getParamsMap(ProceedingJoinPoint point, MethodSignature signature) { + public static Map getParamsMap(JoinPoint point, MethodSignature signature) { Object[] args = point.getArgs(); String[] strings = signature.getParameterNames(); @@ -95,7 +84,7 @@ public static Map getParamsMap(ProceedingJoinPoint point, Method public static AuditOperationType modifyReleaseOperationType(AuditType auditType, Map paramsMap) { switch (auditType.getAuditOperationType()) { case RELEASE: - ReleaseState releaseState = (ReleaseState) paramsMap.get("releaseState"); + ReleaseState releaseState = (ReleaseState) paramsMap.get(Constants.RELEASE_STATE); if (releaseState == null) { break; } @@ -109,7 +98,7 @@ public static AuditOperationType modifyReleaseOperationType(AuditType auditType, } break; case EXECUTE: - ExecuteType executeType = (ExecuteType) paramsMap.get("executeType"); + ExecuteType executeType = (ExecuteType) paramsMap.get(Constants.EXECUTE_TYPE); if (executeType == null) { break; } @@ -184,7 +173,7 @@ public static Map getObjectIfFromReturnObject(Object obj, String } public static boolean isUdfResource(Map paramsMap) { - ResourceType resourceType = (ResourceType) paramsMap.get("type"); + ResourceType resourceType = (ResourceType) paramsMap.get(Constants.STRING_PLUGIN_PARAM_TYPE); return resourceType != null && resourceType.equals(ResourceType.UDF); } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/AuditOperator.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/AuditOperator.java index 7d76d5c4bd2e..c3a9a845f572 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/AuditOperator.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/AuditOperator.java @@ -17,18 +17,16 @@ package org.apache.dolphinscheduler.api.audit.operator; -import org.apache.dolphinscheduler.api.audit.OperatorLog; -import org.apache.dolphinscheduler.api.utils.Result; +import org.apache.dolphinscheduler.api.audit.OperatorLogAspect; +import org.apache.dolphinscheduler.api.audit.enums.AuditType; +import org.apache.dolphinscheduler.dao.entity.AuditLog; +import java.util.List; import java.util.Map; -import io.swagger.v3.oas.annotations.Operation; - public interface AuditOperator { - void recordAudit(Map paramsMap, - Result result, - long latency, - Operation operation, - OperatorLog operatorLog) throws Throwable; + void recordAudit(OperatorLogAspect.AuditContext auditContext, Object returnValue); + + void setRequestParam(AuditType auditType, List auditLogList, Map paramsMap); } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/BaseAuditOperator.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/BaseAuditOperator.java index 270a5e4df41f..0ab607da65fd 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/BaseAuditOperator.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/audit/operator/BaseAuditOperator.java @@ -18,12 +18,12 @@ package org.apache.dolphinscheduler.api.audit.operator; import org.apache.dolphinscheduler.api.audit.OperatorLog; +import org.apache.dolphinscheduler.api.audit.OperatorLogAspect; import org.apache.dolphinscheduler.api.audit.OperatorUtils; import org.apache.dolphinscheduler.api.audit.enums.AuditType; import org.apache.dolphinscheduler.api.service.AuditService; import org.apache.dolphinscheduler.api.utils.Result; import org.apache.dolphinscheduler.dao.entity.AuditLog; -import org.apache.dolphinscheduler.dao.entity.User; import org.apache.commons.lang3.math.NumberUtils; @@ -36,7 +36,6 @@ import org.springframework.stereotype.Service; import com.google.common.base.Strings; -import io.swagger.v3.oas.annotations.Operation; @Service @Slf4j @@ -46,40 +45,34 @@ public abstract class BaseAuditOperator implements AuditOperator { private AuditService auditService; @Override - public void recordAudit(Map paramsMap, - Result result, - long latency, - Operation operation, - OperatorLog operatorLog) { - - AuditType auditType = operatorLog.auditType(); - - User user = OperatorUtils.getUser(paramsMap); - - if (user == null) { - log.error("user is null"); - return; + public void recordAudit(OperatorLogAspect.AuditContext auditContext, Object returnValue) { + Result result = new Result<>(); + + if (returnValue instanceof Result) { + result = (Result) returnValue; + if (OperatorUtils.resultFail(result)) { + log.error("request fail, code {}", result.getCode()); + return; + } } - List auditLogList = OperatorUtils.buildAuditLogList(operation.description(), auditType, user); - setRequestParam(auditType, auditLogList, paramsMap); + long latency = System.currentTimeMillis() - auditContext.getBeginTime(); + List auditLogList = auditContext.getAuditLogList(); - if (OperatorUtils.resultFail(result)) { - log.error("request fail, code {}", result.getCode()); - return; - } + Map paramsMap = auditContext.getParamsMap(); + OperatorLog operatorLog = auditContext.getOperatorLog(); + AuditType auditType = operatorLog.auditType(); setObjectIdentityFromReturnObject(auditType, result, auditLogList); - modifyAuditOperationType(auditType, paramsMap, auditLogList); modifyAuditObjectType(auditType, paramsMap, auditLogList); auditLogList.forEach(auditLog -> auditLog.setLatency(latency)); auditLogList.forEach(auditLog -> auditService.addAudit(auditLog)); - } - protected void setRequestParam(AuditType auditType, List auditLogList, Map paramsMap) { + @Override + public void setRequestParam(AuditType auditType, List auditLogList, Map paramsMap) { String[] paramNameArr = auditType.getRequestParamName(); if (paramNameArr.length == 0) { diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java index 8b01df13194e..71e4d04d6f54 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java @@ -105,8 +105,6 @@ @Slf4j public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDefinitionService { - private static final String RELEASESTATE = "releaseState"; - @Autowired private ProjectMapper projectMapper; @@ -1297,7 +1295,7 @@ public Map releaseTaskDefinition(User loginUser, long projectCod return result; } if (null == releaseState) { - putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, RELEASESTATE); + putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.RELEASE_STATE); return result; } TaskDefinition taskDefinition = taskDefinitionMapper.queryByCode(code); @@ -1337,7 +1335,7 @@ public Map releaseTaskDefinition(User loginUser, long projectCod break; default: log.warn("Parameter releaseState is invalid."); - putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, RELEASESTATE); + putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, Constants.RELEASE_STATE); return result; } int update = taskDefinitionMapper.updateById(taskDefinition); diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java index 19e1a1fabbc7..b5bbf740e9a2 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java @@ -747,4 +747,8 @@ private Constants() { * K8S sensitive param */ public static final String K8S_CONFIG_REGEX = "(?<=((?i)configYaml(\" : \"))).*?(?=(\",\\n))"; + + public static final String RELEASE_STATE = "releaseState"; + public static final String EXECUTE_TYPE = "executeType"; + } diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AuditModelType.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AuditModelType.java index 449f046f4a6e..5f046882c18d 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AuditModelType.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AuditModelType.java @@ -29,7 +29,7 @@ @Getter public enum AuditModelType { - PROJECT("Project", null), // 1 + PROJECT("Project", null), PROCESS("Process", PROJECT), PROCESS_INSTANCE("ProcessInstance", PROCESS), TASK("Task", PROCESS), From a9decc911f8141b7dbc1de8b4f0599e0e170a1ff Mon Sep 17 00:00:00 2001 From: Gallardot Date: Fri, 19 Apr 2024 16:52:17 +0800 Subject: [PATCH 042/165] [Bug][Helm] fix image.registry (#15860) Signed-off-by: Gallardot Co-authored-by: fuchanghai Co-authored-by: Rick Cheng --- deploy/kubernetes/dolphinscheduler/README.md | 2 +- .../kubernetes/dolphinscheduler/values.yaml | 2 +- docs/docs/en/guide/installation/kubernetes.md | 19 +++++++++---------- docs/docs/zh/guide/installation/kubernetes.md | 19 +++++++++---------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/deploy/kubernetes/dolphinscheduler/README.md b/deploy/kubernetes/dolphinscheduler/README.md index 33633f3b2e18..ba533b6e47f9 100644 --- a/deploy/kubernetes/dolphinscheduler/README.md +++ b/deploy/kubernetes/dolphinscheduler/README.md @@ -174,7 +174,7 @@ Please refer to the [Quick Start in Kubernetes](../../../docs/docs/en/guide/inst | image.master | string | `"dolphinscheduler-master"` | master image | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. Options: Always, Never, IfNotPresent | | image.pullSecret | string | `""` | Specify a imagePullSecrets | -| image.registry | string | `"apache/dolphinscheduler"` | Docker image repository for the DolphinScheduler | +| image.registry | string | `"apache"` | Docker image repository for the DolphinScheduler | | image.tag | string | `"latest"` | Docker image version for the DolphinScheduler | | image.tools | string | `"dolphinscheduler-tools"` | tools image | | image.worker | string | `"dolphinscheduler-worker"` | worker image | diff --git a/deploy/kubernetes/dolphinscheduler/values.yaml b/deploy/kubernetes/dolphinscheduler/values.yaml index 98c2f70db07e..7a04ff560440 100644 --- a/deploy/kubernetes/dolphinscheduler/values.yaml +++ b/deploy/kubernetes/dolphinscheduler/values.yaml @@ -31,7 +31,7 @@ initImage: image: # -- Docker image repository for the DolphinScheduler - registry: apache/dolphinscheduler + registry: apache # -- Docker image version for the DolphinScheduler tag: latest # -- Image pull policy. Options: Always, Never, IfNotPresent diff --git a/docs/docs/en/guide/installation/kubernetes.md b/docs/docs/en/guide/installation/kubernetes.md index 8e58fdeccd25..a6587a585511 100644 --- a/docs/docs/en/guide/installation/kubernetes.md +++ b/docs/docs/en/guide/installation/kubernetes.md @@ -14,16 +14,15 @@ If you are a new hand and want to experience DolphinScheduler functions, we reco ## Install DolphinScheduler -Please download the source code package `apache-dolphinscheduler--src.tar.gz`, download address: [download address](https://dolphinscheduler.apache.org/en-us/download) - -To publish the release name `dolphinscheduler` version, please execute the following commands: - -``` -$ tar -zxvf apache-dolphinscheduler--src.tar.gz -$ cd apache-dolphinscheduler--src/deploy/kubernetes/dolphinscheduler -$ helm repo add bitnami https://charts.bitnami.com/bitnami -$ helm dependency update . -$ helm install dolphinscheduler . --set image.tag= +```bash +# Choose the corresponding version yourself +export VERSION=3.2.1 +helm pull oci://registry-1.docker.io/apache/dolphinscheduler-helm --version ${VERSION} +tar -xvf dolphinscheduler-helm-${VERSION}.tgz +cd dolphinscheduler-helm +helm repo add bitnami https://charts.bitnami.com/bitnami +helm dependency update . +helm install dolphinscheduler . ``` To publish the release name `dolphinscheduler` version to `test` namespace: diff --git a/docs/docs/zh/guide/installation/kubernetes.md b/docs/docs/zh/guide/installation/kubernetes.md index 20cd8907e8f3..f4b95de27bb0 100644 --- a/docs/docs/zh/guide/installation/kubernetes.md +++ b/docs/docs/zh/guide/installation/kubernetes.md @@ -14,16 +14,15 @@ Kubernetes 部署目的是在 Kubernetes 集群中部署 DolphinScheduler 服务 ## 安装 dolphinscheduler -请下载源码包 apache-dolphinscheduler--src.tar.gz,下载地址: [下载](https://dolphinscheduler.apache.org/zh-cn/download) - -发布一个名为 `dolphinscheduler` 的版本(release),请执行以下命令: - -``` -$ tar -zxvf apache-dolphinscheduler--src.tar.gz -$ cd apache-dolphinscheduler--src/deploy/kubernetes/dolphinscheduler -$ helm repo add bitnami https://charts.bitnami.com/bitnami -$ helm dependency update . -$ helm install dolphinscheduler . --set image.tag= +```bash +# 自行选择对应的版本 +export VERSION=3.2.1 +helm pull oci://registry-1.docker.io/apache/dolphinscheduler-helm --version ${VERSION} +tar -xvf dolphinscheduler-helm-${VERSION}.tgz +cd dolphinscheduler-helm +helm repo add bitnami https://charts.bitnami.com/bitnami +helm dependency update . +helm install dolphinscheduler . ``` 将名为 `dolphinscheduler` 的版本(release) 发布到 `test` 的命名空间中: From 285c5a8eb541f46b6e7b508729ba4b5178d09152 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Fri, 19 Apr 2024 18:12:40 +0800 Subject: [PATCH 043/165] [DSIP-28] Donnot scan whole bean under classpath (#15874) --- .../dolphinscheduler/alert/AlertServer.java | 13 +- .../api/ApiApplicationServer.java | 26 +- .../api/dto/task/TaskUpdateRequest.java | 5 +- .../impl/ProcessDefinitionServiceImpl.java | 7 +- .../impl/ProcessInstanceServiceImpl.java | 13 +- .../impl/TaskDefinitionServiceImpl.java | 75 ++- .../service/ProcessInstanceServiceTest.java | 38 +- .../TaskDefinitionServiceImplTest.java | 490 +++++++++--------- .../src/test/resources/logback-spring.xml | 57 ++ .../common/CommonConfiguration.java | 26 + .../common/log/remote/RemoteLogUtils.java | 1 - .../common/process/HttpProperty.java | 124 ----- .../api/DatabaseEnvironmentCondition.java | 39 ++ ...java => H2DaoPluginAutoConfiguration.java} | 8 +- .../h2/H2DatabaseEnvironmentCondition.java | 28 + .../main/resources/META-INF/spring.factories | 19 + ...a => MysqlDaoPluginAutoConfiguration.java} | 8 +- .../MysqlDatabaseEnvironmentCondition.java | 28 + .../main/resources/META-INF/spring.factories | 19 + ...PostgresqlDaoPluginAutoConfiguration.java} | 8 +- ...ostgresqlDatabaseEnvironmentCondition.java | 28 + .../main/resources/META-INF/spring.factories | 19 + .../dao/DaoConfiguration.java | 2 +- .../dolphinscheduler/dao/BaseDaoTest.java | 2 - .../dao/repository/impl/AlertDaoTest.java | 2 - .../server/master/MasterServer.java | 28 +- .../runner/TaskExecutionContextFactory.java | 7 +- .../src/test/resources/logback.xml | 2 +- ...ation.java => MeterAutoConfiguration.java} | 13 +- .../meter/metrics/DefaultMetricsProvider.java | 3 - .../main/resources/META-INF/spring.factories | 19 + .../registry/api/RegistryConfiguration.java | 33 ++ .../plugin/registry/etcd/EtcdRegistry.java | 12 +- .../etcd/EtcdRegistryAutoConfiguration.java | 48 ++ .../registry/etcd/EtcdRegistryProperties.java | 2 - .../main/resources/META-INF/spring.factories | 19 + .../jdbc/JdbcRegistryAutoConfiguration.java | 2 + .../registry/zookeeper/ZookeeperRegistry.java | 7 +- .../ZookeeperRegistryAutoConfiguration.java | 46 ++ .../ZookeeperRegistryProperties.java | 94 +--- .../main/resources/META-INF/spring.factories | 19 + ... => QuartzSchedulerAutoConfiguration.java} | 2 +- .../main/resources/META-INF/spring.factories | 19 + .../service/ServiceConfiguration.java | 26 + .../service/process/ProcessServiceImpl.java | 4 - .../service/utils/CommonUtils.java | 108 ---- .../service/process/ProcessServiceTest.java | 5 - .../service/utils/CommonUtilsTest.java | 71 --- .../dolphinscheduler/StandaloneServer.java | 2 +- .../src/main/resources/application.yaml | 2 +- .../plugin/task/api/TaskPluginManager.java | 27 +- .../datasource/UpgradeDolphinScheduler.java | 4 +- .../server/worker/WorkerServer.java | 22 +- .../runner/DefaultWorkerTaskExecutor.java | 3 - .../DefaultWorkerTaskExecutorFactory.java | 5 - .../worker/runner/WorkerTaskExecutor.java | 5 +- .../WorkerTaskExecutorFactoryBuilder.java | 11 - .../runner/DefaultWorkerTaskExecutorTest.java | 5 - .../WorkerTaskExecutorThreadPoolTest.java | 3 +- .../TaskInstanceOperationFunctionTest.java | 5 - .../src/test/resources/logback.xml | 14 +- 61 files changed, 911 insertions(+), 871 deletions(-) create mode 100644 dolphinscheduler-api/src/test/resources/logback-spring.xml create mode 100644 dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/CommonConfiguration.java delete mode 100644 dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/process/HttpProperty.java create mode 100644 dolphinscheduler-dao-plugin/dolphinscheduler-dao-api/src/main/java/org/apache/dolphinscheduler/dao/plugin/api/DatabaseEnvironmentCondition.java rename dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/{H2DaoPluginConfiguration.java => H2DaoPluginAutoConfiguration.java} (88%) create mode 100644 dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DatabaseEnvironmentCondition.java create mode 100644 dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/resources/META-INF/spring.factories rename dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/{MysqlDaoPluginConfiguration.java => MysqlDaoPluginAutoConfiguration.java} (88%) create mode 100644 dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDatabaseEnvironmentCondition.java create mode 100644 dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/resources/META-INF/spring.factories rename dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/{PostgresqlDaoPluginConfiguration.java => PostgresqlDaoPluginAutoConfiguration.java} (88%) create mode 100644 dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDatabaseEnvironmentCondition.java create mode 100644 dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/resources/META-INF/spring.factories rename dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/{MeterConfiguration.java => MeterAutoConfiguration.java} (87%) create mode 100644 dolphinscheduler-meter/src/main/resources/META-INF/spring.factories create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryConfiguration.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryAutoConfiguration.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/resources/META-INF/spring.factories create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryAutoConfiguration.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/resources/META-INF/spring.factories rename dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/{QuartzSchedulerConfiguration.java => QuartzSchedulerAutoConfiguration.java} (97%) create mode 100644 dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/resources/META-INF/spring.factories create mode 100644 dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/ServiceConfiguration.java delete mode 100644 dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/utils/CommonUtils.java delete mode 100644 dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/utils/CommonUtilsTest.java diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java index ff0088ea8d30..55c5c3446c99 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java @@ -23,11 +23,13 @@ import org.apache.dolphinscheduler.alert.rpc.AlertRpcServer; import org.apache.dolphinscheduler.alert.service.AlertBootstrapService; import org.apache.dolphinscheduler.alert.service.ListenerEventPostService; +import org.apache.dolphinscheduler.common.CommonConfiguration; import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.lifecycle.ServerLifeCycleManager; import org.apache.dolphinscheduler.common.thread.DefaultUncaughtExceptionHandler; import org.apache.dolphinscheduler.common.thread.ThreadUtils; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration; +import org.apache.dolphinscheduler.dao.DaoConfiguration; +import org.apache.dolphinscheduler.registry.api.RegistryConfiguration; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -37,14 +39,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; @Slf4j +@Import({CommonConfiguration.class, + DaoConfiguration.class, + RegistryConfiguration.class}) @SpringBootApplication -@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = JdbcRegistryAutoConfiguration.class) -}) public class AlertServer { @Autowired diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/ApiApplicationServer.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/ApiApplicationServer.java index 20a641207694..6f7d8f43d242 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/ApiApplicationServer.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/ApiApplicationServer.java @@ -18,13 +18,17 @@ package org.apache.dolphinscheduler.api; import org.apache.dolphinscheduler.api.metrics.ApiServerMetrics; +import org.apache.dolphinscheduler.common.CommonConfiguration; import org.apache.dolphinscheduler.common.enums.PluginType; import org.apache.dolphinscheduler.common.thread.DefaultUncaughtExceptionHandler; +import org.apache.dolphinscheduler.dao.DaoConfiguration; import org.apache.dolphinscheduler.dao.PluginDao; import org.apache.dolphinscheduler.dao.entity.PluginDefine; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration; +import org.apache.dolphinscheduler.plugin.storage.api.StorageConfiguration; import org.apache.dolphinscheduler.plugin.task.api.TaskChannelFactory; import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; +import org.apache.dolphinscheduler.registry.api.RegistryConfiguration; +import org.apache.dolphinscheduler.service.ServiceConfiguration; import org.apache.dolphinscheduler.spi.params.PluginParamsTransfer; import org.apache.dolphinscheduler.spi.params.base.PluginParams; @@ -38,21 +42,19 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.web.servlet.ServletComponentScan; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; import org.springframework.context.event.EventListener; +@Slf4j +@Import({DaoConfiguration.class, + CommonConfiguration.class, + ServiceConfiguration.class, + StorageConfiguration.class, + RegistryConfiguration.class}) @ServletComponentScan @SpringBootApplication -@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = JdbcRegistryAutoConfiguration.class) -}) -@Slf4j public class ApiApplicationServer { - @Autowired - private TaskPluginManager taskPluginManager; - @Autowired private PluginDao pluginDao; @@ -66,8 +68,8 @@ public static void main(String[] args) { public void run(ApplicationReadyEvent readyEvent) { log.info("Received spring application context ready event will load taskPlugin and write to DB"); // install task plugin - taskPluginManager.loadPlugin(); - for (Map.Entry entry : taskPluginManager.getTaskChannelFactoryMap().entrySet()) { + TaskPluginManager.loadPlugin(); + for (Map.Entry entry : TaskPluginManager.getTaskChannelFactoryMap().entrySet()) { String taskPluginName = entry.getKey(); TaskChannelFactory taskChannelFactory = entry.getValue(); List params = taskChannelFactory.getParams(); diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/task/TaskUpdateRequest.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/task/TaskUpdateRequest.java index b5a8ea7a46fa..d7b026ed160e 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/task/TaskUpdateRequest.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/task/TaskUpdateRequest.java @@ -25,10 +25,10 @@ import org.apache.commons.beanutils.BeanUtils; -import java.lang.reflect.InvocationTargetException; import java.util.Date; import lombok.Data; +import lombok.SneakyThrows; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -107,7 +107,8 @@ public class TaskUpdateRequest { * @param taskDefinition exists task definition object * @return task definition */ - public TaskDefinition mergeIntoTaskDefinition(TaskDefinition taskDefinition) throws InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException { + @SneakyThrows + public TaskDefinition mergeIntoTaskDefinition(TaskDefinition taskDefinition) { TaskDefinition taskDefinitionDeepCopy = (TaskDefinition) BeanUtils.cloneBean(taskDefinition); assert taskDefinitionDeepCopy != null; if (this.name != null) { diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessDefinitionServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessDefinitionServiceImpl.java index 2bf84a0bccb2..e6893b04fbf1 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessDefinitionServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessDefinitionServiceImpl.java @@ -238,9 +238,6 @@ public class ProcessDefinitionServiceImpl extends BaseServiceImpl implements Pro @Autowired private DataSourceMapper dataSourceMapper; - @Autowired - private TaskPluginManager taskPluginManager; - @Autowired private WorkFlowLineageService workFlowLineageService; @@ -424,7 +421,7 @@ private List generateTaskDefinitionList(String taskDefinition throw new ServiceException(Status.DATA_IS_NOT_VALID, taskDefinitionJson); } for (TaskDefinitionLog taskDefinitionLog : taskDefinitionLogs) { - if (!taskPluginManager.checkTaskParameters(ParametersNode.builder() + if (!TaskPluginManager.checkTaskParameters(ParametersNode.builder() .taskType(taskDefinitionLog.getTaskType()) .taskParams(taskDefinitionLog.getTaskParams()) .dependence(taskDefinitionLog.getDependence()) @@ -1618,7 +1615,7 @@ public Map checkProcessNodeList(String processTaskRelationJson, // check whether the process definition json is normal for (TaskNode taskNode : taskNodes) { - if (!taskPluginManager.checkTaskParameters(ParametersNode.builder() + if (!TaskPluginManager.checkTaskParameters(ParametersNode.builder() .taskType(taskNode.getType()) .taskParams(taskNode.getTaskParams()) .dependence(taskNode.getDependence()) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessInstanceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessInstanceServiceImpl.java index 36cc986607ee..06e9fd95db7c 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessInstanceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProcessInstanceServiceImpl.java @@ -70,11 +70,9 @@ import org.apache.dolphinscheduler.dao.mapper.ProcessInstanceMapper; import org.apache.dolphinscheduler.dao.mapper.ProjectMapper; import org.apache.dolphinscheduler.dao.mapper.RelationSubWorkflowMapper; -import org.apache.dolphinscheduler.dao.mapper.ScheduleMapper; import org.apache.dolphinscheduler.dao.mapper.TaskDefinitionLogMapper; import org.apache.dolphinscheduler.dao.mapper.TaskDefinitionMapper; import org.apache.dolphinscheduler.dao.mapper.TaskInstanceMapper; -import org.apache.dolphinscheduler.dao.mapper.TenantMapper; import org.apache.dolphinscheduler.dao.repository.ProcessInstanceDao; import org.apache.dolphinscheduler.dao.repository.ProcessInstanceMapDao; import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao; @@ -177,18 +175,9 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl implements Proce @Autowired UsersService usersService; - @Autowired - private TenantMapper tenantMapper; - @Autowired TaskDefinitionMapper taskDefinitionMapper; - @Autowired - private TaskPluginManager taskPluginManager; - - @Autowired - private ScheduleMapper scheduleMapper; - @Autowired private RelationSubWorkflowMapper relationSubWorkflowMapper; @@ -725,7 +714,7 @@ public Map updateProcessInstance(User loginUser, long projectCod return result; } for (TaskDefinitionLog taskDefinitionLog : taskDefinitionLogs) { - if (!taskPluginManager.checkTaskParameters(ParametersNode.builder() + if (!TaskPluginManager.checkTaskParameters(ParametersNode.builder() .taskType(taskDefinitionLog.getTaskType()) .taskParams(taskDefinitionLog.getTaskParams()) .dependence(taskDefinitionLog.getDependence()) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java index 71e4d04d6f54..2887606a1d28 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/TaskDefinitionServiceImpl.java @@ -75,7 +75,6 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -135,9 +134,6 @@ public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDe @Autowired private ProcessService processService; - @Autowired - private TaskPluginManager taskPluginManager; - @Autowired private ProcessDefinitionService processDefinitionService; @@ -147,8 +143,8 @@ public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDe /** * create task definition * - * @param loginUser login user - * @param projectCode project code + * @param loginUser login user + * @param projectCode project code * @param taskDefinitionJson task definition json */ @Transactional @@ -171,7 +167,7 @@ public Map createTaskDefinition(User loginUser, return result; } for (TaskDefinitionLog taskDefinitionLog : taskDefinitionLogs) { - if (!taskPluginManager.checkTaskParameters(ParametersNode.builder() + if (!TaskPluginManager.checkTaskParameters(ParametersNode.builder() .taskType(taskDefinitionLog.getTaskType()) .taskParams(taskDefinitionLog.getTaskParams()) .dependence(taskDefinitionLog.getDependence()) @@ -212,7 +208,7 @@ private void checkTaskDefinitionValid(User user, TaskDefinition taskDefinition, Project project = projectMapper.queryByCode(taskDefinition.getProjectCode()); projectService.checkProjectAndAuthThrowException(user, project, permissions); - if (!taskPluginManager.checkTaskParameters(ParametersNode.builder() + if (!TaskPluginManager.checkTaskParameters(ParametersNode.builder() .taskType(taskDefinition.getTaskType()) .taskParams(taskDefinition.getTaskParams()) .dependence(taskDefinition.getDependence()) @@ -242,7 +238,7 @@ private ProcessDefinition updateWorkflowLocation(User user, ProcessDefinition pr /** * Create resource task definition * - * @param loginUser login user + * @param loginUser login user * @param taskCreateRequest task definition json * @return new TaskDefinition have created */ @@ -286,11 +282,11 @@ public TaskDefinition createTaskDefinitionV2(User loginUser, /** * create single task definition that binds the workflow * - * @param loginUser login user - * @param projectCode project code + * @param loginUser login user + * @param projectCode project code * @param processDefinitionCode process definition code * @param taskDefinitionJsonObj task definition json object - * @param upstreamCodes upstream task codes, sep comma + * @param upstreamCodes upstream task codes, sep comma * @return create result code */ @Transactional @@ -325,7 +321,7 @@ public Map createTaskBindsWorkFlow(User loginUser, putMsg(result, Status.DATA_IS_NOT_VALID, taskDefinitionJsonObj); return result; } - if (!taskPluginManager.checkTaskParameters(ParametersNode.builder() + if (!TaskPluginManager.checkTaskParameters(ParametersNode.builder() .taskType(taskDefinition.getTaskType()) .taskParams(taskDefinition.getTaskParams()) .dependence(taskDefinition.getDependence()) @@ -412,10 +408,10 @@ public Map createTaskBindsWorkFlow(User loginUser, /** * query task definition * - * @param loginUser login user + * @param loginUser login user * @param projectCode project code * @param processCode process code - * @param taskName task name + * @param taskName task name */ @Override public Map queryTaskDefinitionByName(User loginUser, long projectCode, long processCode, @@ -473,12 +469,12 @@ private void taskCanDeleteValid(User user, TaskDefinition taskDefinition, User l /** * Delete resource task definition by code - * + *

* Only task release state offline and no downstream tasks can be deleted, will also remove the exists * task relation [upstreamTaskCode, taskCode] * * @param loginUser login user - * @param taskCode task code + * @param taskCode task code */ @Transactional @Override @@ -546,9 +542,9 @@ public void updateDag(User loginUser, long processDefinitionCode, /** * update task definition * - * @param loginUser login user - * @param projectCode project code - * @param taskCode task code + * @param loginUser login user + * @param projectCode project code + * @param taskCode task code * @param taskDefinitionJsonObj task definition json object */ @Transactional @@ -604,8 +600,8 @@ private void TaskDefinitionUpdateValid(TaskDefinition taskDefinitionOriginal, Ta /** * update task definition * - * @param loginUser login user - * @param taskCode task code + * @param loginUser login user + * @param taskCode task code * @param taskUpdateRequest task definition json object * @return new TaskDefinition have updated */ @@ -619,13 +615,8 @@ public TaskDefinition updateTaskDefinitionV2(User loginUser, throw new ServiceException(Status.TASK_DEFINITION_NOT_EXISTS, taskCode); } - TaskDefinition taskDefinitionUpdate; - try { - taskDefinitionUpdate = taskUpdateRequest.mergeIntoTaskDefinition(taskDefinitionOriginal); - } catch (InvocationTargetException | IllegalAccessException | InstantiationException - | NoSuchMethodException e) { - throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, taskUpdateRequest.toString()); - } + TaskDefinition taskDefinitionUpdate = taskUpdateRequest.mergeIntoTaskDefinition(taskDefinitionOriginal); + this.checkTaskDefinitionValid(loginUser, taskDefinitionUpdate, TASK_DEFINITION_UPDATE); this.TaskDefinitionUpdateValid(taskDefinitionOriginal, taskDefinitionUpdate); @@ -656,7 +647,7 @@ public TaskDefinition updateTaskDefinitionV2(User loginUser, * Get resource task definition by code * * @param loginUser login user - * @param taskCode task code + * @param taskCode task code * @return TaskDefinition */ @Override @@ -674,7 +665,7 @@ public TaskDefinition getTaskDefinition(User loginUser, /** * Get resource task definition according to query parameter * - * @param loginUser login user + * @param loginUser login user * @param taskFilterRequest taskFilterRequest object you want to filter the resource task definitions * @return TaskDefinitions of page */ @@ -741,7 +732,7 @@ private TaskDefinitionLog updateTask(User loginUser, long projectCode, long task putMsg(result, Status.DATA_IS_NOT_VALID, taskDefinitionJsonObj); return null; } - if (!taskPluginManager.checkTaskParameters(ParametersNode.builder() + if (!TaskPluginManager.checkTaskParameters(ParametersNode.builder() .taskType(taskDefinitionToUpdate.getTaskType()) .taskParams(taskDefinitionToUpdate.getTaskParams()) .dependence(taskDefinitionToUpdate.getDependence()) @@ -846,11 +837,11 @@ private TaskDefinitionLog updateTask(User loginUser, long projectCode, long task /** * update task definition and upstream * - * @param loginUser login user - * @param projectCode project code - * @param taskCode task definition code + * @param loginUser login user + * @param projectCode project code + * @param taskCode task definition code * @param taskDefinitionJsonObj task definition json object - * @param upstreamCodes upstream task codes, sep comma + * @param upstreamCodes upstream task codes, sep comma * @return update result code */ @Override @@ -1019,10 +1010,10 @@ private ProcessTaskRelationLog createProcessTaskRelationLog(User loginUser, /** * switch task definition * - * @param loginUser login user + * @param loginUser login user * @param projectCode project code - * @param taskCode task code - * @param version the version user want to switch + * @param taskCode task code + * @param version the version user want to switch */ @Transactional @Override @@ -1277,9 +1268,9 @@ public Map genTaskCodeList(Integer genNum) { /** * release task definition * - * @param loginUser login user - * @param projectCode project code - * @param code task definition code + * @param loginUser login user + * @param projectCode project code + * @param code task definition code * @param releaseState releaseState * @return update result code */ diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProcessInstanceServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProcessInstanceServiceTest.java index 9fb4d830528d..7d9a4f9338aa 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProcessInstanceServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProcessInstanceServiceTest.java @@ -84,6 +84,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -142,9 +143,6 @@ public class ProcessInstanceServiceTest { @Mock TaskDefinitionMapper taskDefinitionMapper; - @Mock - TaskPluginManager taskPluginManager; - @Mock ScheduleMapper scheduleMapper; @@ -625,21 +623,27 @@ public void testUpdateProcessInstance() { List taskDefinitionLogs = JSONUtils.toList(taskDefinitionJson, TaskDefinitionLog.class); when(processDefinitionService.checkProcessNodeList(taskRelationJson, taskDefinitionLogs)).thenReturn(result); putMsg(result, Status.SUCCESS, projectCode); - when(taskPluginManager.checkTaskParameters(Mockito.any())).thenReturn(true); - Map processInstanceFinishRes = - processInstanceService.updateProcessInstance(loginUser, projectCode, 1, - taskRelationJson, taskDefinitionJson, "2020-02-21 00:00:00", true, "", "", 0); - Assertions.assertEquals(Status.SUCCESS, processInstanceFinishRes.get(Constants.STATUS)); - // success - when(processDefineMapper.queryByCode(46L)).thenReturn(processDefinition); - putMsg(result, Status.SUCCESS, projectCode); - - when(processService.saveProcessDefine(loginUser, processDefinition, Boolean.FALSE, Boolean.FALSE)) - .thenReturn(1); - Map successRes = processInstanceService.updateProcessInstance(loginUser, projectCode, 1, - taskRelationJson, taskDefinitionJson, "2020-02-21 00:00:00", Boolean.FALSE, "", "", 0); - Assertions.assertEquals(Status.SUCCESS, successRes.get(Constants.STATUS)); + try ( + MockedStatic taskPluginManagerMockedStatic = + Mockito.mockStatic(TaskPluginManager.class)) { + taskPluginManagerMockedStatic.when(() -> TaskPluginManager.checkTaskParameters(Mockito.any())) + .thenReturn(true); + Map processInstanceFinishRes = + processInstanceService.updateProcessInstance(loginUser, projectCode, 1, + taskRelationJson, taskDefinitionJson, "2020-02-21 00:00:00", true, "", "", 0); + Assertions.assertEquals(Status.SUCCESS, processInstanceFinishRes.get(Constants.STATUS)); + + // success + when(processDefineMapper.queryByCode(46L)).thenReturn(processDefinition); + putMsg(result, Status.SUCCESS, projectCode); + + when(processService.saveProcessDefine(loginUser, processDefinition, Boolean.FALSE, Boolean.FALSE)) + .thenReturn(1); + Map successRes = processInstanceService.updateProcessInstance(loginUser, projectCode, 1, + taskRelationJson, taskDefinitionJson, "2020-02-21 00:00:00", Boolean.FALSE, "", "", 0); + Assertions.assertEquals(Status.SUCCESS, successRes.get(Constants.STATUS)); + } } @Test diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/TaskDefinitionServiceImplTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/TaskDefinitionServiceImplTest.java index e77eab8029e6..ff8ca6ba2c13 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/TaskDefinitionServiceImplTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/TaskDefinitionServiceImplTest.java @@ -17,13 +17,19 @@ package org.apache.dolphinscheduler.api.service; +import static org.apache.dolphinscheduler.api.AssertionsHelper.assertDoesNotThrow; +import static org.apache.dolphinscheduler.api.AssertionsHelper.assertThrowsServiceException; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.TASK_DEFINITION; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.TASK_DEFINITION_CREATE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.TASK_DEFINITION_DELETE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.TASK_DEFINITION_UPDATE; import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.WORKFLOW_SWITCH_TO_THIS_VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; import org.apache.dolphinscheduler.api.dto.task.TaskCreateRequest; import org.apache.dolphinscheduler.api.dto.task.TaskUpdateRequest; @@ -75,13 +81,17 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class TaskDefinitionServiceImplTest { @InjectMocks @@ -114,9 +124,6 @@ public class TaskDefinitionServiceImplTest { @Mock private ProcessTaskRelationMapper processTaskRelationMapper; - @Mock - private TaskPluginManager taskPluginManager; - @Mock private ProcessTaskRelationService processTaskRelationService; @@ -155,61 +162,73 @@ public void before() { @Test public void createTaskDefinition() { - Project project = getProject(); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); - - Map result = new HashMap<>(); - Mockito.when(projectService.hasProjectAndWritePerm(user, project, result)) - .thenReturn(true); - Mockito.when(taskPluginManager.checkTaskParameters(Mockito.any())).thenReturn(true); + try ( + MockedStatic taskPluginManagerMockedStatic = + Mockito.mockStatic(TaskPluginManager.class)) { + taskPluginManagerMockedStatic.when(() -> TaskPluginManager.checkTaskParameters(Mockito.any())) + .thenReturn(true); + Project project = getProject(); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); + + Map result = new HashMap<>(); + when(projectService.hasProjectAndWritePerm(user, project, result)) + .thenReturn(true); + + String createTaskDefinitionJson = + "[{\"name\":\"detail_up\",\"description\":\"\",\"taskType\":\"SHELL\",\"taskParams\":" + + "\"{\\\"resourceList\\\":[],\\\"localParams\\\":[{\\\"prop\\\":\\\"datetime\\\",\\\"direct\\\":\\\"IN\\\"," + + "\\\"type\\\":\\\"VARCHAR\\\",\\\"value\\\":\\\"${system.datetime}\\\"}],\\\"rawScript\\\":" + + "\\\"echo ${datetime}\\\",\\\"conditionResult\\\":\\\"{\\\\\\\"successNode\\\\\\\":[\\\\\\\"\\\\\\\"]," + + "\\\\\\\"failedNode\\\\\\\":[\\\\\\\"\\\\\\\"]}\\\",\\\"dependence\\\":{}}\",\"flag\":0,\"taskPriority\":0," + + "\"workerGroup\":\"default\",\"failRetryTimes\":0,\"failRetryInterval\":0,\"timeoutFlag\":0," + + "\"timeoutNotifyStrategy\":0,\"timeout\":0,\"delayTime\":0,\"resourceIds\":\"\"}]"; + Map relation = taskDefinitionService + .createTaskDefinition(user, PROJECT_CODE, createTaskDefinitionJson); + assertEquals(Status.SUCCESS, relation.get(Constants.STATUS)); - String createTaskDefinitionJson = - "[{\"name\":\"detail_up\",\"description\":\"\",\"taskType\":\"SHELL\",\"taskParams\":" - + "\"{\\\"resourceList\\\":[],\\\"localParams\\\":[{\\\"prop\\\":\\\"datetime\\\",\\\"direct\\\":\\\"IN\\\"," - + "\\\"type\\\":\\\"VARCHAR\\\",\\\"value\\\":\\\"${system.datetime}\\\"}],\\\"rawScript\\\":" - + "\\\"echo ${datetime}\\\",\\\"conditionResult\\\":\\\"{\\\\\\\"successNode\\\\\\\":[\\\\\\\"\\\\\\\"]," - + "\\\\\\\"failedNode\\\\\\\":[\\\\\\\"\\\\\\\"]}\\\",\\\"dependence\\\":{}}\",\"flag\":0,\"taskPriority\":0," - + "\"workerGroup\":\"default\",\"failRetryTimes\":0,\"failRetryInterval\":0,\"timeoutFlag\":0," - + "\"timeoutNotifyStrategy\":0,\"timeout\":0,\"delayTime\":0,\"resourceIds\":\"\"}]"; - Map relation = taskDefinitionService - .createTaskDefinition(user, PROJECT_CODE, createTaskDefinitionJson); - Assertions.assertEquals(Status.SUCCESS, relation.get(Constants.STATUS)); + } } @Test public void updateTaskDefinition() { - String taskDefinitionJson = getTaskDefinitionJson();; - - Project project = getProject(); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); - - Map result = new HashMap<>(); - putMsg(result, Status.SUCCESS, PROJECT_CODE); - Mockito.when(projectService.hasProjectAndWritePerm(user, project, new HashMap<>())).thenReturn(true); - - Mockito.when(processService.isTaskOnline(TASK_CODE)).thenReturn(Boolean.FALSE); - Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(new TaskDefinition()); - Mockito.when(taskDefinitionMapper.updateById(Mockito.any(TaskDefinitionLog.class))).thenReturn(1); - Mockito.when(taskDefinitionLogMapper.insert(Mockito.any(TaskDefinitionLog.class))).thenReturn(1); - Mockito.when(processTaskRelationLogDao.insert(Mockito.any(ProcessTaskRelationLog.class))).thenReturn(1); - Mockito.when(processDefinitionMapper.queryByCode(2L)).thenReturn(new ProcessDefinition()); - Mockito.when(processDefinitionMapper.updateById(Mockito.any(ProcessDefinition.class))).thenReturn(1); - Mockito.when(processDefinitionLogMapper.insert(Mockito.any(ProcessDefinitionLog.class))).thenReturn(1); - Mockito.when(taskDefinitionLogMapper.queryMaxVersionForDefinition(TASK_CODE)).thenReturn(1); - Mockito.when(taskPluginManager.checkTaskParameters(Mockito.any())).thenReturn(true); - Mockito.when(processTaskRelationMapper.queryProcessTaskRelationByTaskCodeAndTaskVersion(TASK_CODE, 0)) - .thenReturn(getProcessTaskRelationList2()); - Mockito.when(processTaskRelationMapper - .updateProcessTaskRelationTaskVersion(Mockito.any(ProcessTaskRelation.class))).thenReturn(1); - result = taskDefinitionService.updateTaskDefinition(user, PROJECT_CODE, TASK_CODE, taskDefinitionJson); - Assertions.assertEquals(Status.SUCCESS, result.get(Constants.STATUS)); - // failure - Mockito.when(processTaskRelationMapper - .updateProcessTaskRelationTaskVersion(Mockito.any(ProcessTaskRelation.class))).thenReturn(2); - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.updateTaskDefinition(user, PROJECT_CODE, TASK_CODE, taskDefinitionJson)); - Assertions.assertEquals(Status.PROCESS_TASK_RELATION_BATCH_UPDATE_ERROR.getCode(), - ((ServiceException) exception).getCode()); + try ( + MockedStatic taskPluginManagerMockedStatic = + Mockito.mockStatic(TaskPluginManager.class)) { + taskPluginManagerMockedStatic.when(() -> TaskPluginManager.checkTaskParameters(Mockito.any())) + .thenReturn(true); + String taskDefinitionJson = getTaskDefinitionJson(); + + Project project = getProject(); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); + + Map result = new HashMap<>(); + putMsg(result, Status.SUCCESS, PROJECT_CODE); + when(projectService.hasProjectAndWritePerm(user, project, new HashMap<>())).thenReturn(true); + + when(processService.isTaskOnline(TASK_CODE)).thenReturn(Boolean.FALSE); + when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(new TaskDefinition()); + when(taskDefinitionMapper.updateById(Mockito.any(TaskDefinitionLog.class))).thenReturn(1); + when(taskDefinitionLogMapper.insert(Mockito.any(TaskDefinitionLog.class))).thenReturn(1); + when(processTaskRelationLogDao.insert(Mockito.any(ProcessTaskRelationLog.class))).thenReturn(1); + when(processDefinitionMapper.queryByCode(2L)).thenReturn(new ProcessDefinition()); + when(processDefinitionMapper.updateById(Mockito.any(ProcessDefinition.class))).thenReturn(1); + when(processDefinitionLogMapper.insert(Mockito.any(ProcessDefinitionLog.class))).thenReturn(1); + when(taskDefinitionLogMapper.queryMaxVersionForDefinition(TASK_CODE)).thenReturn(1); + when(processTaskRelationMapper.queryProcessTaskRelationByTaskCodeAndTaskVersion(TASK_CODE, 0)) + .thenReturn(getProcessTaskRelationList2()); + when(processTaskRelationMapper + .updateProcessTaskRelationTaskVersion(Mockito.any(ProcessTaskRelation.class))).thenReturn(1); + result = taskDefinitionService.updateTaskDefinition(user, PROJECT_CODE, TASK_CODE, taskDefinitionJson); + assertEquals(Status.SUCCESS, result.get(Constants.STATUS)); + // failure + when(processTaskRelationMapper + .updateProcessTaskRelationTaskVersion(Mockito.any(ProcessTaskRelation.class))).thenReturn(2); + exception = Assertions.assertThrows(ServiceException.class, + () -> taskDefinitionService.updateTaskDefinition(user, PROJECT_CODE, TASK_CODE, + taskDefinitionJson)); + assertEquals(Status.PROCESS_TASK_RELATION_BATCH_UPDATE_ERROR.getCode(), + ((ServiceException) exception).getCode()); + } } @@ -217,72 +236,72 @@ public void updateTaskDefinition() { public void queryTaskDefinitionByName() { String taskName = "task"; Project project = getProject(); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); Map result = new HashMap<>(); putMsg(result, Status.SUCCESS, PROJECT_CODE); - Mockito.when(projectService.checkProjectAndAuth(user, project, PROJECT_CODE, TASK_DEFINITION)) + when(projectService.checkProjectAndAuth(user, project, PROJECT_CODE, TASK_DEFINITION)) .thenReturn(result); - Mockito.when(taskDefinitionMapper.queryByName(project.getCode(), PROCESS_DEFINITION_CODE, taskName)) + when(taskDefinitionMapper.queryByName(project.getCode(), PROCESS_DEFINITION_CODE, taskName)) .thenReturn(new TaskDefinition()); Map relation = taskDefinitionService .queryTaskDefinitionByName(user, PROJECT_CODE, PROCESS_DEFINITION_CODE, taskName); - Assertions.assertEquals(Status.SUCCESS, relation.get(Constants.STATUS)); + assertEquals(Status.SUCCESS, relation.get(Constants.STATUS)); } @Test public void deleteTaskDefinitionByCode() { Project project = getProject(); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); // error task definition not find exception = Assertions.assertThrows(ServiceException.class, () -> taskDefinitionService.deleteTaskDefinitionByCode(user, TASK_CODE)); - Assertions.assertEquals(Status.TASK_DEFINE_NOT_EXIST.getCode(), ((ServiceException) exception).getCode()); + assertEquals(Status.TASK_DEFINE_NOT_EXIST.getCode(), ((ServiceException) exception).getCode()); // error delete single task definition object - Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(getTaskDefinition()); - Mockito.when(taskDefinitionMapper.deleteByCode(TASK_CODE)).thenReturn(0); - Mockito.when(projectService.hasProjectAndWritePerm(user, project, new HashMap<>())).thenReturn(true); + when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(getTaskDefinition()); + when(taskDefinitionMapper.deleteByCode(TASK_CODE)).thenReturn(0); + when(projectService.hasProjectAndWritePerm(user, project, new HashMap<>())).thenReturn(true); exception = Assertions.assertThrows(ServiceException.class, () -> taskDefinitionService.deleteTaskDefinitionByCode(user, TASK_CODE)); - Assertions.assertEquals(Status.DELETE_TASK_DEFINE_BY_CODE_MSG_ERROR.getCode(), + assertEquals(Status.DELETE_TASK_DEFINE_BY_CODE_MSG_ERROR.getCode(), ((ServiceException) exception).getCode()); // success - Mockito.doNothing().when(projectService).checkProjectAndAuthThrowException(user, project, + doNothing().when(projectService).checkProjectAndAuthThrowException(user, project, TASK_DEFINITION_DELETE); - Mockito.when(processTaskRelationMapper.queryDownstreamByTaskCode(TASK_CODE)).thenReturn(new ArrayList<>()); - Mockito.when(taskDefinitionMapper.deleteByCode(TASK_CODE)).thenReturn(1); + when(processTaskRelationMapper.queryDownstreamByTaskCode(TASK_CODE)).thenReturn(new ArrayList<>()); + when(taskDefinitionMapper.deleteByCode(TASK_CODE)).thenReturn(1); Assertions.assertDoesNotThrow(() -> taskDefinitionService.deleteTaskDefinitionByCode(user, TASK_CODE)); } @Test public void switchVersion() { Project project = getProject(); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); Map result = new HashMap<>(); putMsg(result, Status.SUCCESS, PROJECT_CODE); - Mockito.when( + when( projectService.checkProjectAndAuth(user, project, PROJECT_CODE, WORKFLOW_SWITCH_TO_THIS_VERSION)) - .thenReturn(result); + .thenReturn(result); - Mockito.when(taskDefinitionLogMapper.queryByDefinitionCodeAndVersion(TASK_CODE, VERSION)) + when(taskDefinitionLogMapper.queryByDefinitionCodeAndVersion(TASK_CODE, VERSION)) .thenReturn(new TaskDefinitionLog()); TaskDefinition taskDefinition = new TaskDefinition(); taskDefinition.setProjectCode(PROJECT_CODE); - Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)) + when(taskDefinitionMapper.queryByCode(TASK_CODE)) .thenReturn(taskDefinition); - Mockito.when(taskDefinitionMapper.updateById(new TaskDefinitionLog())).thenReturn(1); + when(taskDefinitionMapper.updateById(new TaskDefinitionLog())).thenReturn(1); Map relation = taskDefinitionService .switchVersion(user, PROJECT_CODE, TASK_CODE, VERSION); - Assertions.assertEquals(Status.SUCCESS, relation.get(Constants.STATUS)); + assertEquals(Status.SUCCESS, relation.get(Constants.STATUS)); } private void putMsg(Map result, Status status, Object... statusParams) { @@ -331,7 +350,7 @@ public void checkJson() { @Test public void genTaskCodeList() { Map genTaskCodeList = taskDefinitionService.genTaskCodeList(10); - Assertions.assertEquals(Status.SUCCESS, genTaskCodeList.get(Constants.STATUS)); + assertEquals(Status.SUCCESS, genTaskCodeList.get(Constants.STATUS)); } @Test @@ -348,31 +367,31 @@ public void testQueryTaskDefinitionListPaging() { taskMainInfo.setUpstreamTaskName("4"); taskMainInfoIPage.setRecords(Collections.singletonList(taskMainInfo)); taskMainInfoIPage.setTotal(10L); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); - Mockito.when(projectService.checkProjectAndAuth(user, project, PROJECT_CODE, TASK_DEFINITION)) + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(project); + when(projectService.checkProjectAndAuth(user, project, PROJECT_CODE, TASK_DEFINITION)) .thenReturn(checkResult); - Mockito.when(taskDefinitionMapper.queryDefineListPaging(Mockito.any(Page.class), Mockito.anyLong(), + when(taskDefinitionMapper.queryDefineListPaging(Mockito.any(Page.class), Mockito.anyLong(), Mockito.isNull(), Mockito.anyString(), Mockito.isNull())) - .thenReturn(taskMainInfoIPage); - Mockito.when(taskDefinitionMapper.queryDefineListByCodeList(PROJECT_CODE, Collections.singletonList(3L))) + .thenReturn(taskMainInfoIPage); + when(taskDefinitionMapper.queryDefineListByCodeList(PROJECT_CODE, Collections.singletonList(3L))) .thenReturn(Collections.singletonList(taskMainInfo)); Result result = taskDefinitionService.queryTaskDefinitionListPaging(user, PROJECT_CODE, null, null, null, pageNo, pageSize); - Assertions.assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); + assertEquals(Status.SUCCESS.getMsg(), result.getMsg()); } @Test public void testReleaseTaskDefinition() { - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(getProject()); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(getProject()); Project project = getProject(); // check task dose not exist Map result = new HashMap<>(); putMsg(result, Status.TASK_DEFINE_NOT_EXIST, TASK_CODE); - Mockito.when(projectService.checkProjectAndAuth(user, project, PROJECT_CODE, null)).thenReturn(result); + when(projectService.checkProjectAndAuth(user, project, PROJECT_CODE, null)).thenReturn(result); Map map = taskDefinitionService.releaseTaskDefinition(user, PROJECT_CODE, TASK_CODE, ReleaseState.OFFLINE); - Assertions.assertEquals(Status.TASK_DEFINE_NOT_EXIST, map.get(Constants.STATUS)); + assertEquals(Status.TASK_DEFINE_NOT_EXIST, map.get(Constants.STATUS)); // process definition offline putMsg(result, Status.SUCCESS); @@ -384,23 +403,23 @@ public void testReleaseTaskDefinition() { "{\"resourceList\":[],\"localParams\":[],\"rawScript\":\"echo 1\",\"conditionResult\":{\"successNode\":[\"\"],\"failedNode\":[\"\"]},\"dependence\":{}}"; taskDefinition.setTaskParams(params); taskDefinition.setTaskType("SHELL"); - Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(taskDefinition); + when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(taskDefinition); TaskDefinitionLog taskDefinitionLog = new TaskDefinitionLog(taskDefinition); - Mockito.when(taskDefinitionLogMapper.queryByDefinitionCodeAndVersion(TASK_CODE, taskDefinition.getVersion())) + when(taskDefinitionLogMapper.queryByDefinitionCodeAndVersion(TASK_CODE, taskDefinition.getVersion())) .thenReturn(taskDefinitionLog); Map offlineTaskResult = taskDefinitionService.releaseTaskDefinition(user, PROJECT_CODE, TASK_CODE, ReleaseState.OFFLINE); - Assertions.assertEquals(Status.SUCCESS, offlineTaskResult.get(Constants.STATUS)); + assertEquals(Status.SUCCESS, offlineTaskResult.get(Constants.STATUS)); // process definition online, resource exist Map onlineTaskResult = taskDefinitionService.releaseTaskDefinition(user, PROJECT_CODE, TASK_CODE, ReleaseState.ONLINE); - Assertions.assertEquals(Status.SUCCESS, onlineTaskResult.get(Constants.STATUS)); + assertEquals(Status.SUCCESS, onlineTaskResult.get(Constants.STATUS)); // release error code Map failResult = taskDefinitionService.releaseTaskDefinition(user, PROJECT_CODE, TASK_CODE, ReleaseState.getEnum(2)); - Assertions.assertEquals(Status.REQUEST_PARAMS_NOT_VALID_ERROR, failResult.get(Constants.STATUS)); + assertEquals(Status.REQUEST_PARAMS_NOT_VALID_ERROR, failResult.get(Constants.STATUS)); } @Test @@ -410,133 +429,127 @@ public void testCreateTaskDefinitionV2() { taskCreateRequest.setWorkflowCode(PROCESS_DEFINITION_CODE); // error process definition not find - exception = Assertions.assertThrows(ServiceException.class, + assertThrowsServiceException(Status.PROCESS_DEFINE_NOT_EXIST, () -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); - Assertions.assertEquals(Status.PROCESS_DEFINE_NOT_EXIST.getCode(), ((ServiceException) exception).getCode()); // error project not find - Mockito.when(processDefinitionMapper.queryByCode(PROCESS_DEFINITION_CODE)).thenReturn(getProcessDefinition()); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(getProject()); - Mockito.doThrow(new ServiceException(Status.PROJECT_NOT_EXIST)).when(projectService) + when(processDefinitionMapper.queryByCode(PROCESS_DEFINITION_CODE)).thenReturn(getProcessDefinition()); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(getProject()); + doThrow(new ServiceException(Status.PROJECT_NOT_EXIST)).when(projectService) .checkProjectAndAuthThrowException(user, getProject(), TASK_DEFINITION_CREATE); - exception = Assertions.assertThrows(ServiceException.class, + assertThrowsServiceException(Status.PROJECT_NOT_EXIST, () -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); - Assertions.assertEquals(Status.PROJECT_NOT_EXIST.getCode(), ((ServiceException) exception).getCode()); // error task definition taskCreateRequest.setTaskParams(TASK_PARAMETER); - Mockito.doNothing().when(projectService).checkProjectAndAuthThrowException(user, getProject(), - TASK_DEFINITION_CREATE); - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); - Assertions.assertEquals(Status.PROCESS_NODE_S_PARAMETER_INVALID.getCode(), - ((ServiceException) exception).getCode()); - - // error create task definition object - Mockito.when(taskPluginManager.checkTaskParameters(Mockito.any())).thenReturn(true); - Mockito.when(taskDefinitionMapper.insert(isA(TaskDefinition.class))).thenReturn(0); - exception = Assertions.assertThrows(ServiceException.class, + doNothing().when(projectService).checkProjectAndAuthThrowException(user, getProject(), TASK_DEFINITION_CREATE); + assertThrowsServiceException(Status.PROCESS_NODE_S_PARAMETER_INVALID, () -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); - Assertions.assertEquals(Status.CREATE_TASK_DEFINITION_ERROR.getCode(), - ((ServiceException) exception).getCode()); - // error sync to task definition log - Mockito.when(taskDefinitionMapper.insert(isA(TaskDefinition.class))).thenReturn(1); - Mockito.when(taskDefinitionLogMapper.insert(isA(TaskDefinitionLog.class))).thenReturn(0); - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); - Assertions.assertEquals(Status.CREATE_TASK_DEFINITION_LOG_ERROR.getCode(), - ((ServiceException) exception).getCode()); - - // success - Mockito.when(taskDefinitionLogMapper.insert(isA(TaskDefinitionLog.class))).thenReturn(1); - // we do not test updateUpstreamTaskDefinition, because it should be tested in processTaskRelationService - Mockito.when( - processTaskRelationService.updateUpstreamTaskDefinitionWithSyncDag(isA(User.class), isA(Long.class), - isA(Boolean.class), - isA(TaskRelationUpdateUpstreamRequest.class))) - .thenReturn(getProcessTaskRelationList()); - Mockito.when(processDefinitionService.updateSingleProcessDefinition(isA(User.class), isA(Long.class), - isA(WorkflowUpdateRequest.class))).thenReturn(getProcessDefinition()); - Assertions.assertDoesNotThrow(() -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); + try ( + MockedStatic taskPluginManagerMockedStatic = + Mockito.mockStatic(TaskPluginManager.class)) { + taskPluginManagerMockedStatic.when(() -> TaskPluginManager.checkTaskParameters(Mockito.any())) + .thenReturn(true); + + // error create task definition object + when(taskDefinitionMapper.insert(isA(TaskDefinition.class))).thenReturn(0); + assertThrowsServiceException(Status.CREATE_TASK_DEFINITION_ERROR, + () -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); + + // error sync to task definition log + when(taskDefinitionMapper.insert(isA(TaskDefinition.class))).thenReturn(1); + when(taskDefinitionLogMapper.insert(isA(TaskDefinitionLog.class))).thenReturn(0); + assertThrowsServiceException(Status.CREATE_TASK_DEFINITION_LOG_ERROR, + () -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); + + // success + when(taskDefinitionLogMapper.insert(isA(TaskDefinitionLog.class))).thenReturn(1); + // we do not test updateUpstreamTaskDefinition, because it should be tested in processTaskRelationService + when( + processTaskRelationService.updateUpstreamTaskDefinitionWithSyncDag(isA(User.class), isA(Long.class), + isA(Boolean.class), + isA(TaskRelationUpdateUpstreamRequest.class))) + .thenReturn(getProcessTaskRelationList()); + when(processDefinitionService.updateSingleProcessDefinition(isA(User.class), isA(Long.class), + isA(WorkflowUpdateRequest.class))).thenReturn(getProcessDefinition()); + assertDoesNotThrow(() -> taskDefinitionService.createTaskDefinitionV2(user, taskCreateRequest)); + } } @Test public void testUpdateTaskDefinitionV2() { TaskUpdateRequest taskUpdateRequest = new TaskUpdateRequest(); + TaskDefinition taskDefinition = getTaskDefinition(); + Project project = getProject(); // error task definition not exists - exception = Assertions.assertThrows(ServiceException.class, + assertThrowsServiceException(Status.TASK_DEFINITION_NOT_EXISTS, () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); - Assertions.assertEquals(Status.TASK_DEFINITION_NOT_EXISTS.getCode(), ((ServiceException) exception).getCode()); // error project not find - Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(getTaskDefinition()); - Mockito.when(projectMapper.queryByCode(isA(Long.class))).thenReturn(getProject()); - Mockito.doThrow(new ServiceException(Status.PROJECT_NOT_EXIST)).when(projectService) - .checkProjectAndAuthThrowException(user, getProject(), TASK_DEFINITION_UPDATE); - exception = Assertions.assertThrows(ServiceException.class, + when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(taskDefinition); + when(projectMapper.queryByCode(isA(Long.class))).thenReturn(project); + doThrow(new ServiceException(Status.PROJECT_NOT_EXIST)).when(projectService) + .checkProjectAndAuthThrowException(user, project, TASK_DEFINITION_UPDATE); + assertThrowsServiceException(Status.PROJECT_NOT_EXIST, () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); - Assertions.assertEquals(Status.PROJECT_NOT_EXIST.getCode(), ((ServiceException) exception).getCode()); // error task definition - Mockito.doNothing().when(projectService).checkProjectAndAuthThrowException(user, getProject(), - TASK_DEFINITION_UPDATE); - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); - Assertions.assertEquals(Status.PROCESS_NODE_S_PARAMETER_INVALID.getCode(), - ((ServiceException) exception).getCode()); - - // error task definition already online - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); - Assertions.assertEquals(Status.PROCESS_NODE_S_PARAMETER_INVALID.getCode(), - ((ServiceException) exception).getCode()); - - // error task definition nothing update - Mockito.when(processService.isTaskOnline(TASK_CODE)).thenReturn(false); - Mockito.when(taskPluginManager.checkTaskParameters(Mockito.any())).thenReturn(true); - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); - Assertions.assertEquals(Status.TASK_DEFINITION_NOT_CHANGE.getCode(), ((ServiceException) exception).getCode()); - - // error task definition version invalid - taskUpdateRequest.setTaskPriority(String.valueOf(Priority.HIGH)); - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); - Assertions.assertEquals(Status.DATA_IS_NOT_VALID.getCode(), ((ServiceException) exception).getCode()); - - // error task definition update effect number - Mockito.when(taskDefinitionLogMapper.queryMaxVersionForDefinition(TASK_CODE)).thenReturn(VERSION); - Mockito.when(taskDefinitionMapper.updateById(isA(TaskDefinition.class))).thenReturn(0); - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); - Assertions.assertEquals(Status.UPDATE_TASK_DEFINITION_ERROR.getCode(), - ((ServiceException) exception).getCode()); - - // error task definition log insert - Mockito.when(taskDefinitionMapper.updateById(isA(TaskDefinition.class))).thenReturn(1); - Mockito.when(taskDefinitionLogMapper.insert(isA(TaskDefinitionLog.class))).thenReturn(0); - exception = Assertions.assertThrows(ServiceException.class, - () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); - Assertions.assertEquals(Status.CREATE_TASK_DEFINITION_LOG_ERROR.getCode(), - ((ServiceException) exception).getCode()); - - // success - Mockito.when(taskDefinitionLogMapper.insert(isA(TaskDefinitionLog.class))).thenReturn(1); - // we do not test updateUpstreamTaskDefinition, because it should be tested in processTaskRelationService - Mockito.when( - processTaskRelationService.updateUpstreamTaskDefinitionWithSyncDag(isA(User.class), isA(Long.class), - isA(Boolean.class), - isA(TaskRelationUpdateUpstreamRequest.class))) - .thenReturn(getProcessTaskRelationList()); - Assertions.assertDoesNotThrow( - () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); + doNothing().when(projectService).checkProjectAndAuthThrowException(user, project, TASK_DEFINITION_UPDATE); + + try ( + MockedStatic taskPluginManagerMockedStatic = + Mockito.mockStatic(TaskPluginManager.class)) { + taskPluginManagerMockedStatic.when(() -> TaskPluginManager.checkTaskParameters(Mockito.any())) + .thenReturn(false); + assertThrowsServiceException(Status.PROCESS_NODE_S_PARAMETER_INVALID, + () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); + } - TaskDefinition taskDefinition = - taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest); - Assertions.assertEquals(getTaskDefinition().getVersion() + 1, taskDefinition.getVersion()); + try ( + MockedStatic taskPluginManagerMockedStatic = + Mockito.mockStatic(TaskPluginManager.class)) { + taskPluginManagerMockedStatic.when(() -> TaskPluginManager.checkTaskParameters(Mockito.any())) + .thenReturn(true); + // error task definition nothing update + when(processService.isTaskOnline(TASK_CODE)).thenReturn(false); + assertThrowsServiceException(Status.TASK_DEFINITION_NOT_CHANGE, + () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); + + // error task definition version invalid + taskUpdateRequest.setTaskPriority(String.valueOf(Priority.HIGH)); + assertThrowsServiceException(Status.DATA_IS_NOT_VALID, + () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); + + // error task definition update effect number + when(taskDefinitionLogMapper.queryMaxVersionForDefinition(TASK_CODE)).thenReturn(VERSION); + when(taskDefinitionMapper.updateById(isA(TaskDefinition.class))).thenReturn(0); + assertThrowsServiceException(Status.UPDATE_TASK_DEFINITION_ERROR, + () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); + + // error task definition log insert + when(taskDefinitionMapper.updateById(isA(TaskDefinition.class))).thenReturn(1); + when(taskDefinitionLogMapper.insert(isA(TaskDefinitionLog.class))).thenReturn(0); + assertThrowsServiceException(Status.CREATE_TASK_DEFINITION_LOG_ERROR, + () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); + + // success + when(taskDefinitionLogMapper.insert(isA(TaskDefinitionLog.class))).thenReturn(1); + // we do not test updateUpstreamTaskDefinition, because it should be tested in processTaskRelationService + when( + processTaskRelationService.updateUpstreamTaskDefinitionWithSyncDag(isA(User.class), isA(Long.class), + isA(Boolean.class), + isA(TaskRelationUpdateUpstreamRequest.class))) + .thenReturn(getProcessTaskRelationList()); + Assertions.assertDoesNotThrow( + () -> taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest)); + + taskDefinition = + taskDefinitionService.updateTaskDefinitionV2(user, TASK_CODE, taskUpdateRequest); + assertEquals(getTaskDefinition().getVersion() + 1, taskDefinition.getVersion()); + } } @Test @@ -549,28 +562,28 @@ public void testUpdateDag() { ArrayList taskDefinitionLogs = new ArrayList<>(); taskDefinitionLogs.add(taskDefinitionLog); Integer version = 1; - Mockito.when(processDefinitionMapper.queryByCode(isA(long.class))).thenReturn(processDefinition); + when(processDefinitionMapper.queryByCode(isA(long.class))).thenReturn(processDefinition); // saveProcessDefine - Mockito.when(processDefineLogMapper.queryMaxVersionForDefinition(isA(long.class))).thenReturn(version); - Mockito.when(processDefineLogMapper.insert(isA(ProcessDefinitionLog.class))).thenReturn(1); - Mockito.when(processDefinitionMapper.insert(isA(ProcessDefinitionLog.class))).thenReturn(1); + when(processDefineLogMapper.queryMaxVersionForDefinition(isA(long.class))).thenReturn(version); + when(processDefineLogMapper.insert(isA(ProcessDefinitionLog.class))).thenReturn(1); + when(processDefinitionMapper.insert(isA(ProcessDefinitionLog.class))).thenReturn(1); int insertVersion = processServiceImpl.saveProcessDefine(loginUser, processDefinition, Boolean.TRUE, Boolean.TRUE); - Mockito.when(processService.saveProcessDefine(loginUser, processDefinition, Boolean.TRUE, Boolean.TRUE)) + when(processService.saveProcessDefine(loginUser, processDefinition, Boolean.TRUE, Boolean.TRUE)) .thenReturn(insertVersion); - Assertions.assertEquals(insertVersion, version + 1); + assertEquals(insertVersion, version + 1); // saveTaskRelation List processTaskRelationLogList = getProcessTaskRelationLogList(); - Mockito.when(processTaskRelationMapper.queryByProcessCode(eq(processDefinition.getCode()))) + when(processTaskRelationMapper.queryByProcessCode(eq(processDefinition.getCode()))) .thenReturn(processTaskRelationList); - Mockito.when(processTaskRelationMapper.batchInsert(isA(List.class))).thenReturn(1); - Mockito.when(processTaskRelationLogMapper.batchInsert(isA(List.class))).thenReturn(1); + when(processTaskRelationMapper.batchInsert(isA(List.class))).thenReturn(1); + when(processTaskRelationLogMapper.batchInsert(isA(List.class))).thenReturn(1); int insertResult = processServiceImpl.saveTaskRelation(loginUser, processDefinition.getProjectCode(), processDefinition.getCode(), insertVersion, processTaskRelationLogList, taskDefinitionLogs, Boolean.TRUE); - Assertions.assertEquals(Constants.EXIT_CODE_SUCCESS, insertResult); + assertEquals(Constants.EXIT_CODE_SUCCESS, insertResult); Assertions.assertDoesNotThrow( () -> taskDefinitionService.updateDag(loginUser, processDefinition.getCode(), processTaskRelationList, taskDefinitionLogs)); @@ -581,55 +594,60 @@ public void testGetTaskDefinition() { // error task definition not exists exception = Assertions.assertThrows(ServiceException.class, () -> taskDefinitionService.getTaskDefinition(user, TASK_CODE)); - Assertions.assertEquals(Status.TASK_DEFINE_NOT_EXIST.getCode(), ((ServiceException) exception).getCode()); + assertEquals(Status.TASK_DEFINE_NOT_EXIST.getCode(), ((ServiceException) exception).getCode()); // error task definition not exists - Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(getTaskDefinition()); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(getProject()); - Mockito.doThrow(new ServiceException(Status.USER_NO_OPERATION_PROJECT_PERM)).when(projectService) + when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(getTaskDefinition()); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(getProject()); + doThrow(new ServiceException(Status.USER_NO_OPERATION_PROJECT_PERM)).when(projectService) .checkProjectAndAuthThrowException(user, getProject(), TASK_DEFINITION); exception = Assertions.assertThrows(ServiceException.class, () -> taskDefinitionService.getTaskDefinition(user, TASK_CODE)); - Assertions.assertEquals(Status.USER_NO_OPERATION_PROJECT_PERM.getCode(), + assertEquals(Status.USER_NO_OPERATION_PROJECT_PERM.getCode(), ((ServiceException) exception).getCode()); // success - Mockito.doNothing().when(projectService).checkProjectAndAuthThrowException(user, getProject(), TASK_DEFINITION); + doNothing().when(projectService).checkProjectAndAuthThrowException(user, getProject(), TASK_DEFINITION); Assertions.assertDoesNotThrow(() -> taskDefinitionService.getTaskDefinition(user, TASK_CODE)); } @Test public void testUpdateTaskWithUpstream() { - - String taskDefinitionJson = getTaskDefinitionJson(); - TaskDefinition taskDefinition = getTaskDefinition(); - taskDefinition.setFlag(Flag.NO); - TaskDefinition taskDefinitionSecond = getTaskDefinition(); - taskDefinitionSecond.setCode(5); - - user.setUserType(UserType.ADMIN_USER); - Mockito.when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(getProject()); - Mockito.when(projectService.hasProjectAndWritePerm(user, getProject(), new HashMap<>())).thenReturn(true); - Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(taskDefinition); - Mockito.when(taskPluginManager.checkTaskParameters(Mockito.any())).thenReturn(true); - Mockito.when(taskDefinitionLogMapper.queryMaxVersionForDefinition(TASK_CODE)).thenReturn(1); - Mockito.when(taskDefinitionMapper.updateById(Mockito.any())).thenReturn(1); - Mockito.when(taskDefinitionLogMapper.insert(Mockito.any())).thenReturn(1); - - Mockito.when(taskDefinitionMapper.queryByCodeList(Mockito.anySet())) - .thenReturn(Arrays.asList(taskDefinition, taskDefinitionSecond)); - - Mockito.when(processTaskRelationMapper.queryUpstreamByCode(PROJECT_CODE, TASK_CODE)) - .thenReturn(getProcessTaskRelationListV2()); - Mockito.when(processDefinitionMapper.queryByCode(PROCESS_DEFINITION_CODE)).thenReturn(getProcessDefinition()); - Mockito.when(processTaskRelationMapper.batchInsert(Mockito.anyList())).thenReturn(1); - Mockito.when(processTaskRelationMapper.updateById(Mockito.any())).thenReturn(1); - Mockito.when(processTaskRelationLogDao.batchInsert(Mockito.anyList())).thenReturn(2); - // success - Map successMap = taskDefinitionService.updateTaskWithUpstream(user, PROJECT_CODE, TASK_CODE, - taskDefinitionJson, UPSTREAM_CODE); - Assertions.assertEquals(Status.SUCCESS, successMap.get(Constants.STATUS)); - user.setUserType(UserType.GENERAL_USER); + try ( + MockedStatic taskPluginManagerMockedStatic = + Mockito.mockStatic(TaskPluginManager.class)) { + taskPluginManagerMockedStatic.when(() -> TaskPluginManager.checkTaskParameters(Mockito.any())) + .thenReturn(true); + String taskDefinitionJson = getTaskDefinitionJson(); + TaskDefinition taskDefinition = getTaskDefinition(); + taskDefinition.setFlag(Flag.NO); + TaskDefinition taskDefinitionSecond = getTaskDefinition(); + taskDefinitionSecond.setCode(5); + + user.setUserType(UserType.ADMIN_USER); + when(projectMapper.queryByCode(PROJECT_CODE)).thenReturn(getProject()); + when(projectService.hasProjectAndWritePerm(user, getProject(), new HashMap<>())).thenReturn(true); + when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(taskDefinition); + when(taskDefinitionLogMapper.queryMaxVersionForDefinition(TASK_CODE)).thenReturn(1); + when(taskDefinitionMapper.updateById(Mockito.any())).thenReturn(1); + when(taskDefinitionLogMapper.insert(Mockito.any())).thenReturn(1); + + when(taskDefinitionMapper.queryByCodeList(Mockito.anySet())) + .thenReturn(Arrays.asList(taskDefinition, taskDefinitionSecond)); + + when(processTaskRelationMapper.queryUpstreamByCode(PROJECT_CODE, TASK_CODE)) + .thenReturn(getProcessTaskRelationListV2()); + when(processDefinitionMapper.queryByCode(PROCESS_DEFINITION_CODE)) + .thenReturn(getProcessDefinition()); + when(processTaskRelationMapper.batchInsert(Mockito.anyList())).thenReturn(1); + when(processTaskRelationMapper.updateById(Mockito.any())).thenReturn(1); + when(processTaskRelationLogDao.batchInsert(Mockito.anyList())).thenReturn(2); + // success + Map successMap = taskDefinitionService.updateTaskWithUpstream(user, PROJECT_CODE, TASK_CODE, + taskDefinitionJson, UPSTREAM_CODE); + assertEquals(Status.SUCCESS, successMap.get(Constants.STATUS)); + user.setUserType(UserType.GENERAL_USER); + } } private String getTaskDefinitionJson() { diff --git a/dolphinscheduler-api/src/test/resources/logback-spring.xml b/dolphinscheduler-api/src/test/resources/logback-spring.xml new file mode 100644 index 000000000000..9159c3b02d49 --- /dev/null +++ b/dolphinscheduler-api/src/test/resources/logback-spring.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + [%level] %date{yyyy-MM-dd HH:mm:ss.SSS Z} %logger{10}:[%line] - %msg%n + + UTF-8 + + + + + ${log.base}/dolphinscheduler-api.log + + ${log.base}/dolphinscheduler-api.%d{yyyy-MM-dd_HH}.%i.log + 168 + 64MB + 50GB + true + + + + [%level] %date{yyyy-MM-dd HH:mm:ss.SSS Z} %logger{10}:[%line] - %msg%n + + UTF-8 + + + + + + + + + + + + diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/CommonConfiguration.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/CommonConfiguration.java new file mode 100644 index 000000000000..5411e4cbc154 --- /dev/null +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/CommonConfiguration.java @@ -0,0 +1,26 @@ +/* + * 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.common; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("org.apache.dolphinscheduler.common") +public class CommonConfiguration { +} diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/RemoteLogUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/RemoteLogUtils.java index 25d80244740e..8aab70f30445 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/RemoteLogUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/log/remote/RemoteLogUtils.java @@ -40,7 +40,6 @@ public class RemoteLogUtils { @Autowired private RemoteLogService autowiredRemoteLogService; - @PostConstruct private void init() { remoteLogService = autowiredRemoteLogService; diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/process/HttpProperty.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/process/HttpProperty.java deleted file mode 100644 index 11786fd5a3d6..000000000000 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/process/HttpProperty.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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.common.process; - -import org.apache.dolphinscheduler.common.enums.HttpParametersType; - -import java.util.Objects; - -public class HttpProperty { - - /** - * key - */ - private String prop; - - /** - * httpParametersType - */ - private HttpParametersType httpParametersType; - - /** - * value - */ - private String value; - - public HttpProperty() { - } - - public HttpProperty(String prop, HttpParametersType httpParametersType, String value) { - this.prop = prop; - this.httpParametersType = httpParametersType; - this.value = value; - } - - /** - * getter method - * - * @return the prop - * @see HttpProperty#prop - */ - public String getProp() { - return prop; - } - - /** - * setter method - * - * @param prop the prop to set - * @see HttpProperty#prop - */ - public void setProp(String prop) { - this.prop = prop; - } - - /** - * getter method - * - * @return the value - * @see HttpProperty#value - */ - public String getValue() { - return value; - } - - /** - * setter method - * - * @param value the value to set - * @see HttpProperty#value - */ - public void setValue(String value) { - this.value = value; - } - - public HttpParametersType getHttpParametersType() { - return httpParametersType; - } - - public void setHttpParametersType(HttpParametersType httpParametersType) { - this.httpParametersType = httpParametersType; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - HttpProperty property = (HttpProperty) o; - return Objects.equals(prop, property.prop) - && Objects.equals(value, property.value); - } - - @Override - public int hashCode() { - return Objects.hash(prop, value); - } - - @Override - public String toString() { - return "HttpProperty{" - + "prop='" + prop + '\'' - + ", httpParametersType=" + httpParametersType - + ", value='" + value + '\'' - + '}'; - } -} diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-api/src/main/java/org/apache/dolphinscheduler/dao/plugin/api/DatabaseEnvironmentCondition.java b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-api/src/main/java/org/apache/dolphinscheduler/dao/plugin/api/DatabaseEnvironmentCondition.java new file mode 100644 index 000000000000..32fb807a9bac --- /dev/null +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-api/src/main/java/org/apache/dolphinscheduler/dao/plugin/api/DatabaseEnvironmentCondition.java @@ -0,0 +1,39 @@ +/* + * 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.dao.plugin.api; + +import java.util.Arrays; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class DatabaseEnvironmentCondition implements Condition { + + private final String profile; + + public DatabaseEnvironmentCondition(String profile) { + this.profile = profile; + } + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + String[] activeProfiles = context.getEnvironment().getActiveProfiles(); + return Arrays.asList(activeProfiles).contains(profile); + } +} diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DaoPluginConfiguration.java b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DaoPluginAutoConfiguration.java similarity index 88% rename from dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DaoPluginConfiguration.java rename to dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DaoPluginAutoConfiguration.java index 9aea94f77de1..f496679ed5a7 100644 --- a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DaoPluginConfiguration.java +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DaoPluginAutoConfiguration.java @@ -29,14 +29,14 @@ import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; import com.baomidou.mybatisplus.annotation.DbType; -@Profile("h2") -@Configuration -public class H2DaoPluginConfiguration implements DaoPluginConfiguration { +@Conditional(H2DatabaseEnvironmentCondition.class) +@Configuration(proxyBeanMethods = false) +public class H2DaoPluginAutoConfiguration implements DaoPluginConfiguration { @Autowired private DataSource dataSource; diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DatabaseEnvironmentCondition.java b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DatabaseEnvironmentCondition.java new file mode 100644 index 000000000000..894f38bd205f --- /dev/null +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/java/org/apache/dolphinscheduler/dao/plugin/h2/H2DatabaseEnvironmentCondition.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.dolphinscheduler.dao.plugin.h2; + +import org.apache.dolphinscheduler.dao.plugin.api.DatabaseEnvironmentCondition; + +public class H2DatabaseEnvironmentCondition extends DatabaseEnvironmentCondition { + + public H2DatabaseEnvironmentCondition() { + super("h2"); + } + +} diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/resources/META-INF/spring.factories b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..c899dfb43fa3 --- /dev/null +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-h2/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.apache.dolphinscheduler.dao.plugin.h2.H2DaoPluginAutoConfiguration diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDaoPluginConfiguration.java b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDaoPluginAutoConfiguration.java similarity index 88% rename from dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDaoPluginConfiguration.java rename to dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDaoPluginAutoConfiguration.java index 8b37fca67b56..5fb3a350aef8 100644 --- a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDaoPluginConfiguration.java +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDaoPluginAutoConfiguration.java @@ -28,14 +28,14 @@ import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; import com.baomidou.mybatisplus.annotation.DbType; -@Profile("mysql") -@Configuration -public class MysqlDaoPluginConfiguration implements DaoPluginConfiguration { +@Configuration(proxyBeanMethods = false) +@Conditional(MysqlDatabaseEnvironmentCondition.class) +public class MysqlDaoPluginAutoConfiguration implements DaoPluginConfiguration { @Autowired private DataSource dataSource; diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDatabaseEnvironmentCondition.java b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDatabaseEnvironmentCondition.java new file mode 100644 index 000000000000..2136e1354e77 --- /dev/null +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/java/org/apache/dolphinscheduler/dao/plugin/mysql/MysqlDatabaseEnvironmentCondition.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.dolphinscheduler.dao.plugin.mysql; + +import org.apache.dolphinscheduler.dao.plugin.api.DatabaseEnvironmentCondition; + +public class MysqlDatabaseEnvironmentCondition extends DatabaseEnvironmentCondition { + + public MysqlDatabaseEnvironmentCondition() { + super("mysql"); + } + +} diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/resources/META-INF/spring.factories b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..386c80c67686 --- /dev/null +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-mysql/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.apache.dolphinscheduler.dao.plugin.mysql.MysqlDaoPluginAutoConfiguration diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDaoPluginConfiguration.java b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDaoPluginAutoConfiguration.java similarity index 88% rename from dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDaoPluginConfiguration.java rename to dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDaoPluginAutoConfiguration.java index e57c84fab9d5..f0467bfd0747 100644 --- a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDaoPluginConfiguration.java +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDaoPluginAutoConfiguration.java @@ -29,14 +29,14 @@ import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; import com.baomidou.mybatisplus.annotation.DbType; -@Profile("postgresql") -@Configuration -public class PostgresqlDaoPluginConfiguration implements DaoPluginConfiguration { +@Conditional(PostgresqlDatabaseEnvironmentCondition.class) +@Configuration(proxyBeanMethods = false) +public class PostgresqlDaoPluginAutoConfiguration implements DaoPluginConfiguration { @Autowired private DataSource dataSource; diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDatabaseEnvironmentCondition.java b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDatabaseEnvironmentCondition.java new file mode 100644 index 000000000000..2c71ea844280 --- /dev/null +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/java/org/apache/dolphinscheduler/dao/plugin/postgresql/PostgresqlDatabaseEnvironmentCondition.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.dolphinscheduler.dao.plugin.postgresql; + +import org.apache.dolphinscheduler.dao.plugin.api.DatabaseEnvironmentCondition; + +public class PostgresqlDatabaseEnvironmentCondition extends DatabaseEnvironmentCondition { + + public PostgresqlDatabaseEnvironmentCondition() { + super("postgresql"); + } + +} diff --git a/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/resources/META-INF/spring.factories b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..fd6a5f07b9a9 --- /dev/null +++ b/dolphinscheduler-dao-plugin/dolphinscheduler-dao-postgresql/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.apache.dolphinscheduler.dao.plugin.postgresql.PostgresqlDaoPluginAutoConfiguration diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/DaoConfiguration.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/DaoConfiguration.java index 985f5b56b4a5..e089c8086bbf 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/DaoConfiguration.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/DaoConfiguration.java @@ -40,8 +40,8 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; @Configuration +@ComponentScan("org.apache.dolphinscheduler.dao") @EnableAutoConfiguration -@ComponentScan({"org.apache.dolphinscheduler.dao.plugin"}) @MapperScan(basePackages = "org.apache.dolphinscheduler.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactory") public class DaoConfiguration { diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/BaseDaoTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/BaseDaoTest.java index 231c947f6588..6f14eb436e75 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/BaseDaoTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/BaseDaoTest.java @@ -22,7 +22,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; -import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; @ExtendWith(MockitoExtension.class) @@ -30,6 +29,5 @@ @SpringBootApplication(scanBasePackageClasses = DaoConfiguration.class) @Transactional @Rollback -@EnableTransactionManagement public abstract class BaseDaoTest { } diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java index c0a841c1a063..fe8545f3e2e0 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java @@ -34,7 +34,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; @ActiveProfiles(ProfileType.H2) @@ -43,7 +42,6 @@ @SpringBootTest(classes = DaoConfiguration.class) @Transactional @Rollback -@EnableTransactionManagement public class AlertDaoTest { @Autowired diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java index fd262a8c6e06..79885701355d 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/MasterServer.java @@ -17,15 +17,18 @@ package org.apache.dolphinscheduler.server.master; +import org.apache.dolphinscheduler.common.CommonConfiguration; import org.apache.dolphinscheduler.common.IStoppable; import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.lifecycle.ServerLifeCycleManager; import org.apache.dolphinscheduler.common.thread.DefaultUncaughtExceptionHandler; import org.apache.dolphinscheduler.common.thread.ThreadUtils; +import org.apache.dolphinscheduler.dao.DaoConfiguration; import org.apache.dolphinscheduler.meter.metrics.MetricsProvider; import org.apache.dolphinscheduler.meter.metrics.SystemMetrics; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration; +import org.apache.dolphinscheduler.plugin.storage.api.StorageConfiguration; import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; +import org.apache.dolphinscheduler.registry.api.RegistryConfiguration; import org.apache.dolphinscheduler.scheduler.api.SchedulerApi; import org.apache.dolphinscheduler.server.master.metrics.MasterServerMetrics; import org.apache.dolphinscheduler.server.master.registry.MasterRegistryClient; @@ -35,6 +38,7 @@ import org.apache.dolphinscheduler.server.master.runner.FailoverExecuteThread; import org.apache.dolphinscheduler.server.master.runner.MasterSchedulerBootstrap; import org.apache.dolphinscheduler.server.master.runner.taskgroup.TaskGroupCoordinator; +import org.apache.dolphinscheduler.service.ServiceConfiguration; import org.apache.dolphinscheduler.service.bean.SpringApplicationContext; import javax.annotation.PostConstruct; @@ -45,18 +49,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.context.annotation.Import; -@SpringBootApplication -@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = JdbcRegistryAutoConfiguration.class) -}) -@EnableTransactionManagement -@EnableCaching @Slf4j +@Import({DaoConfiguration.class, + ServiceConfiguration.class, + CommonConfiguration.class, + StorageConfiguration.class, + RegistryConfiguration.class}) +@SpringBootApplication public class MasterServer implements IStoppable { @Autowired @@ -65,9 +66,6 @@ public class MasterServer implements IStoppable { @Autowired private MasterRegistryClient masterRegistryClient; - @Autowired - private TaskPluginManager taskPluginManager; - @Autowired private MasterSchedulerBootstrap masterSchedulerBootstrap; @@ -109,7 +107,7 @@ public void run() throws SchedulerException { this.masterRPCServer.start(); // install task plugin - this.taskPluginManager.loadPlugin(); + TaskPluginManager.loadPlugin(); this.masterSlotManager.start(); diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/TaskExecutionContextFactory.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/TaskExecutionContextFactory.java index ab1806ff6720..b3b0cf67ea8d 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/TaskExecutionContextFactory.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/TaskExecutionContextFactory.java @@ -90,9 +90,6 @@ public class TaskExecutionContextFactory { @Autowired private ProcessService processService; - @Autowired - private TaskPluginManager taskPluginManager; - @Autowired private CuringParamsService curingParamsService; @@ -106,14 +103,14 @@ public TaskExecutionContext createTaskExecutionContext(TaskInstance taskInstance ProcessInstance workflowInstance = taskInstance.getProcessInstance(); ResourceParametersHelper resources = - Optional.ofNullable(taskPluginManager.getTaskChannel(taskInstance.getTaskType())) + Optional.ofNullable(TaskPluginManager.getTaskChannel(taskInstance.getTaskType())) .map(taskChannel -> taskChannel.getResources(taskInstance.getTaskParams())) .orElse(null); setTaskResourceInfo(resources); Map businessParamsMap = curingParamsService.preBuildBusinessParams(workflowInstance); - AbstractParameters baseParam = taskPluginManager.getParameters(ParametersNode.builder() + AbstractParameters baseParam = TaskPluginManager.getParameters(ParametersNode.builder() .taskType(taskInstance.getTaskType()).taskParams(taskInstance.getTaskParams()).build()); Map propertyMap = curingParamsService.paramParsingPreparation(taskInstance, baseParam, workflowInstance); diff --git a/dolphinscheduler-master/src/test/resources/logback.xml b/dolphinscheduler-master/src/test/resources/logback.xml index 4470a4639109..286e35cd1fb1 100644 --- a/dolphinscheduler-master/src/test/resources/logback.xml +++ b/dolphinscheduler-master/src/test/resources/logback.xml @@ -65,7 +65,7 @@ - + diff --git a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/MeterConfiguration.java b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/MeterAutoConfiguration.java similarity index 87% rename from dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/MeterConfiguration.java rename to dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/MeterAutoConfiguration.java index e3b140a57842..fa8933d13297 100644 --- a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/MeterConfiguration.java +++ b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/MeterAutoConfiguration.java @@ -20,7 +20,8 @@ package org.apache.dolphinscheduler.meter; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.apache.dolphinscheduler.meter.metrics.DefaultMetricsProvider; + import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -42,11 +43,15 @@ * } * */ -@Configuration +@Configuration(proxyBeanMethods = false) @EnableAspectJAutoProxy -@EnableAutoConfiguration @ConditionalOnProperty(prefix = "metrics", name = "enabled", havingValue = "true") -public class MeterConfiguration { +public class MeterAutoConfiguration { + + @Bean + public DefaultMetricsProvider metricsProvider(MeterRegistry meterRegistry) { + return new DefaultMetricsProvider(meterRegistry); + } @Bean public TimedAspect timedAspect(MeterRegistry registry) { diff --git a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/DefaultMetricsProvider.java b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/DefaultMetricsProvider.java index f1240a054117..e293e44a8d81 100644 --- a/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/DefaultMetricsProvider.java +++ b/dolphinscheduler-meter/src/main/java/org/apache/dolphinscheduler/meter/metrics/DefaultMetricsProvider.java @@ -19,11 +19,8 @@ import org.apache.dolphinscheduler.common.utils.OSUtils; -import org.springframework.stereotype.Component; - import io.micrometer.core.instrument.MeterRegistry; -@Component public class DefaultMetricsProvider implements MetricsProvider { private final MeterRegistry meterRegistry; diff --git a/dolphinscheduler-meter/src/main/resources/META-INF/spring.factories b/dolphinscheduler-meter/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..77bc56d86eb7 --- /dev/null +++ b/dolphinscheduler-meter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.apache.dolphinscheduler.meter.MeterAutoConfiguration diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryConfiguration.java b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryConfiguration.java new file mode 100644 index 000000000000..bae36949645a --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryConfiguration.java @@ -0,0 +1,33 @@ +/* + * 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.registry.api; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RegistryConfiguration { + + @Bean + @ConditionalOnMissingBean + public RegistryClient registryClient(Registry registry) { + return new RegistryClient(registry); + } + +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java index 1d1397db54e9..6833a6607b17 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java @@ -41,8 +41,6 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import com.google.common.base.Splitter; @@ -68,8 +66,6 @@ * This is one of the implementation of {@link Registry}, with this implementation, you need to rely on Etcd cluster to * store the DolphinScheduler master/worker's metadata and do the server registry/unRegistry. */ -@Component -@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "etcd") @Slf4j public class EtcdRegistry implements Registry { @@ -86,6 +82,7 @@ public class EtcdRegistry implements Registry { private final Map watcherMap = new ConcurrentHashMap<>(); private static final long TIME_TO_LIVE_SECONDS = 30L; + public EtcdRegistry(EtcdRegistryProperties registryProperties) throws SSLException { ClientBuilder clientBuilder = Client.builder() .endpoints(Util.toURIs(Splitter.on(",").trimResults().splitToList(registryProperties.getEndpoints()))) @@ -141,8 +138,7 @@ public void connectUntilTimeout(@NonNull Duration timeout) throws RegistryExcept } /** - * - * @param path The prefix of the key being listened to + * @param path The prefix of the key being listened to * @param listener * @return if subcribe Returns true if no exception was thrown */ @@ -165,8 +161,8 @@ public boolean subscribe(String path, SubscribeListener listener) { } /** - * @throws throws an exception if the unsubscribe path does not exist * @param path The prefix of the key being listened to + * @throws throws an exception if the unsubscribe path does not exist */ @Override public void unsubscribe(String path) { @@ -184,7 +180,6 @@ public void addConnectionStateListener(ConnectionListener listener) { } /** - * * @return Returns the value corresponding to the key * @throws throws an exception if the key does not exist */ @@ -202,7 +197,6 @@ public String get(String key) { } /** - * * @param deleteOnDisconnect Does the put data disappear when the client disconnects */ @Override diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryAutoConfiguration.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryAutoConfiguration.java new file mode 100644 index 000000000000..1038d312ffc7 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryAutoConfiguration.java @@ -0,0 +1,48 @@ +/* + * 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.registry.etcd; + +import org.apache.dolphinscheduler.registry.api.Registry; + +import javax.net.ssl.SSLException; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@ComponentScan +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "etcd") +public class EtcdRegistryAutoConfiguration { + + public EtcdRegistryAutoConfiguration() { + log.info("Load EtcdRegistryAutoConfiguration"); + } + + @Bean + @ConditionalOnMissingBean(value = Registry.class) + public EtcdRegistry etcdRegistry(EtcdRegistryProperties etcdRegistryProperties) throws SSLException { + return new EtcdRegistry(etcdRegistryProperties); + } + +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryProperties.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryProperties.java index faded2a506a4..babb6dea7637 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryProperties.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryProperties.java @@ -21,13 +21,11 @@ import lombok.Data; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Data @Configuration -@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "etcd") @ConfigurationProperties(prefix = "registry") public class EtcdRegistryProperties { diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/resources/META-INF/spring.factories b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..689817bb9678 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.apache.dolphinscheduler.plugin.registry.etcd.EtcdRegistryAutoConfiguration diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java index 09211f99fbbf..f21ce0d67caa 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; @@ -37,6 +38,7 @@ import com.zaxxer.hikari.HikariDataSource; @Slf4j +@ComponentScan @Configuration(proxyBeanMethods = false) @MapperScan("org.apache.dolphinscheduler.plugin.registry.jdbc.mapper") @ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "jdbc") diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistry.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistry.java index 7333c10f0583..3f0c3ccb59df 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistry.java @@ -50,14 +50,11 @@ import javax.annotation.PostConstruct; import lombok.NonNull; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; import com.google.common.base.Strings; -@Component -@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "zookeeper") +@Slf4j public final class ZookeeperRegistry implements Registry { private final ZookeeperRegistryProperties.ZookeeperProperties properties; diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryAutoConfiguration.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryAutoConfiguration.java new file mode 100644 index 000000000000..e8fc31327e8e --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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.registry.zookeeper; + +import org.apache.dolphinscheduler.registry.api.Registry; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@ComponentScan +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "zookeeper") +public class ZookeeperRegistryAutoConfiguration { + + public ZookeeperRegistryAutoConfiguration() { + log.info("Load ZookeeperRegistryAutoConfiguration"); + } + + @Bean + @ConditionalOnMissingBean(value = Registry.class) + public ZookeeperRegistry zookeeperRegistry(ZookeeperRegistryProperties zookeeperRegistryProperties) { + return new ZookeeperRegistry(zookeeperRegistryProperties); + } + +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryProperties.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryProperties.java index 42dbc5d256f0..b54ebc0b1a3e 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryProperties.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryProperties.java @@ -19,25 +19,19 @@ import java.time.Duration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import lombok.Data; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +@Data @Configuration -@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "zookeeper") @ConfigurationProperties(prefix = "registry") public class ZookeeperRegistryProperties { private ZookeeperProperties zookeeper = new ZookeeperProperties(); - public ZookeeperProperties getZookeeper() { - return zookeeper; - } - - public void setZookeeper(ZookeeperProperties zookeeper) { - this.zookeeper = zookeeper; - } - + @Data public static final class ZookeeperProperties { private String namespace; @@ -48,91 +42,13 @@ public static final class ZookeeperProperties { private Duration connectionTimeout = Duration.ofSeconds(9); private Duration blockUntilConnected = Duration.ofMillis(600); - public String getNamespace() { - return namespace; - } - - public void setNamespace(String namespace) { - this.namespace = namespace; - } - - public String getConnectString() { - return connectString; - } - - public void setConnectString(String connectString) { - this.connectString = connectString; - } - - public RetryPolicy getRetryPolicy() { - return retryPolicy; - } - - public void setRetryPolicy(RetryPolicy retryPolicy) { - this.retryPolicy = retryPolicy; - } - - public String getDigest() { - return digest; - } - - public void setDigest(String digest) { - this.digest = digest; - } - - public Duration getSessionTimeout() { - return sessionTimeout; - } - - public void setSessionTimeout(Duration sessionTimeout) { - this.sessionTimeout = sessionTimeout; - } - - public Duration getConnectionTimeout() { - return connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getBlockUntilConnected() { - return blockUntilConnected; - } - - public void setBlockUntilConnected(Duration blockUntilConnected) { - this.blockUntilConnected = blockUntilConnected; - } - + @Data public static final class RetryPolicy { private Duration baseSleepTime = Duration.ofMillis(60); private int maxRetries; private Duration maxSleep = Duration.ofMillis(300); - public Duration getBaseSleepTime() { - return baseSleepTime; - } - - public void setBaseSleepTime(Duration baseSleepTime) { - this.baseSleepTime = baseSleepTime; - } - - public int getMaxRetries() { - return maxRetries; - } - - public void setMaxRetries(int maxRetries) { - this.maxRetries = maxRetries; - } - - public Duration getMaxSleep() { - return maxSleep; - } - - public void setMaxSleep(Duration maxSleep) { - this.maxSleep = maxSleep; - } } } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/resources/META-INF/spring.factories b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..821f1a70c139 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.apache.dolphinscheduler.plugin.registry.zookeeper.ZookeeperRegistryAutoConfiguration diff --git a/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSchedulerConfiguration.java b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSchedulerAutoConfiguration.java similarity index 97% rename from dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSchedulerConfiguration.java rename to dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSchedulerAutoConfiguration.java index 07ff8af43445..34fb78258772 100644 --- a/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSchedulerConfiguration.java +++ b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSchedulerAutoConfiguration.java @@ -28,7 +28,7 @@ @AutoConfiguration(after = {QuartzAutoConfiguration.class}) @ConditionalOnClass(value = Scheduler.class) -public class QuartzSchedulerConfiguration { +public class QuartzSchedulerAutoConfiguration { @Bean @ConditionalOnMissingBean diff --git a/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/resources/META-INF/spring.factories b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..b34f896b84af --- /dev/null +++ b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# 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. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.apache.dolphinscheduler.scheduler.quartz.QuartzSchedulerAutoConfiguration diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/ServiceConfiguration.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/ServiceConfiguration.java new file mode 100644 index 000000000000..fa831a5b6bb8 --- /dev/null +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/ServiceConfiguration.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.service; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@ComponentScan("org.apache.dolphinscheduler.service") +@Configuration +public class ServiceConfiguration { +} diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java index 3c207ae9829a..635948fc860f 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/ProcessServiceImpl.java @@ -111,7 +111,6 @@ import org.apache.dolphinscheduler.extract.common.ILogService; import org.apache.dolphinscheduler.extract.master.ITaskInstanceExecutionEventListener; import org.apache.dolphinscheduler.extract.master.transportor.WorkflowInstanceStateChangeEvent; -import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.plugin.task.api.enums.Direct; import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus; import org.apache.dolphinscheduler.plugin.task.api.enums.dp.DqTaskState; @@ -262,9 +261,6 @@ public class ProcessServiceImpl implements ProcessService { @Autowired private WorkFlowLineageMapper workFlowLineageMapper; - @Autowired - private TaskPluginManager taskPluginManager; - @Autowired private ClusterMapper clusterMapper; diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/utils/CommonUtils.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/utils/CommonUtils.java deleted file mode 100644 index e00212823a80..000000000000 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/utils/CommonUtils.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.dolphinscheduler.service.utils; - -import org.apache.dolphinscheduler.common.constants.Constants; -import org.apache.dolphinscheduler.common.constants.DataSourceConstants; -import org.apache.dolphinscheduler.common.utils.PropertyUtils; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang3.StringUtils; - -import java.net.URL; -import java.nio.charset.StandardCharsets; - -import lombok.extern.slf4j.Slf4j; - -/** - * common utils - */ -@Slf4j -public class CommonUtils { - - private static final Base64 BASE64 = new Base64(); - - protected CommonUtils() { - throw new UnsupportedOperationException("Construct CommonUtils"); - } - - /** - * @return get the path of system environment variables - */ - public static String getSystemEnvPath() { - String envPath = PropertyUtils.getString(Constants.DOLPHINSCHEDULER_ENV_PATH); - if (StringUtils.isEmpty(envPath)) { - URL envDefaultPath = CommonUtils.class.getClassLoader().getResource(Constants.ENV_PATH); - - if (envDefaultPath != null) { - envPath = envDefaultPath.getPath(); - log.debug("env path :{}", envPath); - } else { - envPath = "/etc/profile"; - } - } - - return envPath; - } - - /** - * encode password - */ - public static String encodePassword(String password) { - if (StringUtils.isEmpty(password)) { - return StringUtils.EMPTY; - } - // if encryption is not turned on, return directly - boolean encryptionEnable = PropertyUtils.getBoolean(DataSourceConstants.DATASOURCE_ENCRYPTION_ENABLE, false); - if (!encryptionEnable) { - return password; - } - - // Using Base64 + salt to process password - String salt = PropertyUtils.getString(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT, - DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT); - String passwordWithSalt = salt + new String(BASE64.encode(password.getBytes(StandardCharsets.UTF_8))); - return new String(BASE64.encode(passwordWithSalt.getBytes(StandardCharsets.UTF_8))); - } - - /** - * decode password - */ - public static String decodePassword(String password) { - if (StringUtils.isEmpty(password)) { - return StringUtils.EMPTY; - } - - // if encryption is not turned on, return directly - boolean encryptionEnable = PropertyUtils.getBoolean(DataSourceConstants.DATASOURCE_ENCRYPTION_ENABLE, false); - if (!encryptionEnable) { - return password; - } - - // Using Base64 + salt to process password - String salt = PropertyUtils.getString(DataSourceConstants.DATASOURCE_ENCRYPTION_SALT, - DataSourceConstants.DATASOURCE_ENCRYPTION_SALT_DEFAULT); - String passwordWithSalt = new String(BASE64.decode(password), StandardCharsets.UTF_8); - if (!passwordWithSalt.startsWith(salt)) { - log.warn("There is a password and salt mismatch: {} ", password); - return password; - } - return new String(BASE64.decode(passwordWithSalt.substring(salt.length())), StandardCharsets.UTF_8); - } - -} diff --git a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/process/ProcessServiceTest.java b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/process/ProcessServiceTest.java index 509f9a4cf115..5c998e1f9fd0 100644 --- a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/process/ProcessServiceTest.java +++ b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/process/ProcessServiceTest.java @@ -79,7 +79,6 @@ import org.apache.dolphinscheduler.plugin.task.api.enums.dp.OptionSourceType; import org.apache.dolphinscheduler.plugin.task.api.model.Property; import org.apache.dolphinscheduler.plugin.task.api.model.ResourceInfo; -import org.apache.dolphinscheduler.service.cron.CronUtilsTest; import org.apache.dolphinscheduler.service.exceptions.CronParseException; import org.apache.dolphinscheduler.service.exceptions.ServiceException; import org.apache.dolphinscheduler.service.expand.CuringParamsService; @@ -102,8 +101,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * process service test @@ -112,7 +109,6 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class ProcessServiceTest { - private static final Logger logger = LoggerFactory.getLogger(CronUtilsTest.class); @InjectMocks private ProcessServiceImpl processService; @Mock @@ -667,7 +663,6 @@ public void testSaveTaskDefine() { taskDefinition.setVersion(1); taskDefinition.setCreateTime(new Date()); taskDefinition.setUpdateTime(new Date()); - when(taskPluginManager.getParameters(any())).thenReturn(null); when(taskDefinitionLogMapper.queryByDefinitionCodeAndVersion(taskDefinition.getCode(), taskDefinition.getVersion())).thenReturn(taskDefinition); when(taskDefinitionLogMapper.queryMaxVersionForDefinition(taskDefinition.getCode())).thenReturn(1); diff --git a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/utils/CommonUtilsTest.java b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/utils/CommonUtilsTest.java deleted file mode 100644 index cab280a7020d..000000000000 --- a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/utils/CommonUtilsTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.dolphinscheduler.service.utils; - -import org.apache.dolphinscheduler.common.utils.FileUtils; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * configuration test - */ -@ExtendWith(MockitoExtension.class) -public class CommonUtilsTest { - - private static final Logger logger = LoggerFactory.getLogger(CommonUtilsTest.class); - - @Test - public void getSystemEnvPath() { - String envPath; - envPath = CommonUtils.getSystemEnvPath(); - Assertions.assertEquals("/etc/profile", envPath); - } - - @Test - public void getDownloadFilename() { - logger.info(FileUtils.getDownloadFilename("a.txt")); - Assertions.assertTrue(true); - } - - @Test - public void getUploadFilename() { - logger.info(FileUtils.getUploadFilename("1234", "a.txt")); - Assertions.assertTrue(true); - } - - @Test - public void test() { - InetAddress ip; - try { - ip = InetAddress.getLocalHost(); - logger.info(ip.getHostAddress()); - } catch (UnknownHostException e) { - e.printStackTrace(); - } - Assertions.assertTrue(true); - } - -} diff --git a/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/StandaloneServer.java b/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/StandaloneServer.java index 14808ab62939..a39d3c9a2200 100644 --- a/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/StandaloneServer.java +++ b/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/StandaloneServer.java @@ -24,8 +24,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication @Slf4j +@SpringBootApplication public class StandaloneServer { public static void main(String[] args) throws Exception { diff --git a/dolphinscheduler-standalone-server/src/main/resources/application.yaml b/dolphinscheduler-standalone-server/src/main/resources/application.yaml index 654113471de5..5122eea2a17c 100644 --- a/dolphinscheduler-standalone-server/src/main/resources/application.yaml +++ b/dolphinscheduler-standalone-server/src/main/resources/application.yaml @@ -329,4 +329,4 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/dolphinscheduler?useUnicode=true&characterEncoding=UTF-8 username: root - password: root@123 + password: root diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/TaskPluginManager.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/TaskPluginManager.java index be3417466ffe..938aa77459fc 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/TaskPluginManager.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/TaskPluginManager.java @@ -36,21 +36,18 @@ import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -@Component @Slf4j public class TaskPluginManager { - private final Map taskChannelFactoryMap = new HashMap<>(); - private final Map taskChannelMap = new HashMap<>(); + private static final Map taskChannelFactoryMap = new HashMap<>(); + private static final Map taskChannelMap = new HashMap<>(); - private final AtomicBoolean loadedFlag = new AtomicBoolean(false); + private static final AtomicBoolean loadedFlag = new AtomicBoolean(false); /** * Load task plugins from classpath. */ - public void loadPlugin() { + public static void loadPlugin() { if (!loadedFlag.compareAndSet(false, true)) { log.warn("The task plugin has already been loaded"); return; @@ -70,24 +67,24 @@ public void loadPlugin() { } - public Map getTaskChannelMap() { + public static Map getTaskChannelMap() { return Collections.unmodifiableMap(taskChannelMap); } - public Map getTaskChannelFactoryMap() { + public static Map getTaskChannelFactoryMap() { return Collections.unmodifiableMap(taskChannelFactoryMap); } - public TaskChannel getTaskChannel(String type) { - return this.getTaskChannelMap().get(type); + public static TaskChannel getTaskChannel(String type) { + return getTaskChannelMap().get(type); } - public boolean checkTaskParameters(ParametersNode parametersNode) { - AbstractParameters abstractParameters = this.getParameters(parametersNode); + public static boolean checkTaskParameters(ParametersNode parametersNode) { + AbstractParameters abstractParameters = getParameters(parametersNode); return abstractParameters != null && abstractParameters.checkParameters(); } - public AbstractParameters getParameters(ParametersNode parametersNode) { + public static AbstractParameters getParameters(ParametersNode parametersNode) { String taskType = parametersNode.getTaskType(); if (Objects.isNull(taskType)) { return null; @@ -106,7 +103,7 @@ public AbstractParameters getParameters(ParametersNode parametersNode) { case TaskConstants.TASK_TYPE_DYNAMIC: return JSONUtils.parseObject(parametersNode.getTaskParams(), DynamicParameters.class); default: - TaskChannel taskChannel = this.getTaskChannelMap().get(taskType); + TaskChannel taskChannel = getTaskChannelMap().get(taskType); if (Objects.isNull(taskChannel)) { return null; } diff --git a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/UpgradeDolphinScheduler.java b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/UpgradeDolphinScheduler.java index aa9553a30822..0588b20ddac5 100644 --- a/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/UpgradeDolphinScheduler.java +++ b/dolphinscheduler-tools/src/main/java/org/apache/dolphinscheduler/tools/datasource/UpgradeDolphinScheduler.java @@ -23,12 +23,12 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -@ImportAutoConfiguration(DaoConfiguration.class) +@Import(DaoConfiguration.class) @SpringBootApplication public class UpgradeDolphinScheduler { diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java index b0866f7fd8ff..8dd842a8eda3 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/WorkerServer.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.server.worker; +import org.apache.dolphinscheduler.common.CommonConfiguration; import org.apache.dolphinscheduler.common.IStoppable; import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.lifecycle.ServerLifeCycleManager; @@ -24,11 +25,12 @@ import org.apache.dolphinscheduler.common.thread.ThreadUtils; import org.apache.dolphinscheduler.meter.metrics.MetricsProvider; import org.apache.dolphinscheduler.meter.metrics.SystemMetrics; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryAutoConfiguration; +import org.apache.dolphinscheduler.plugin.storage.api.StorageConfiguration; import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.plugin.task.api.utils.LogUtils; import org.apache.dolphinscheduler.plugin.task.api.utils.ProcessUtils; +import org.apache.dolphinscheduler.registry.api.RegistryConfiguration; import org.apache.dolphinscheduler.server.worker.message.MessageRetryRunner; import org.apache.dolphinscheduler.server.worker.metrics.WorkerServerMetrics; import org.apache.dolphinscheduler.server.worker.registry.WorkerRegistryClient; @@ -47,24 +49,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.context.annotation.Import; -@SpringBootApplication -@EnableTransactionManagement -@ComponentScan(value = "org.apache.dolphinscheduler", excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = JdbcRegistryAutoConfiguration.class) -}) @Slf4j +@Import({CommonConfiguration.class, + StorageConfiguration.class, + RegistryConfiguration.class}) +@SpringBootApplication public class WorkerServer implements IStoppable { @Autowired private WorkerRegistryClient workerRegistryClient; - @Autowired - private TaskPluginManager taskPluginManager; - @Autowired private WorkerRpcServer workerRpcServer; @@ -89,7 +85,7 @@ public static void main(String[] args) { @PostConstruct public void run() { this.workerRpcServer.start(); - this.taskPluginManager.loadPlugin(); + TaskPluginManager.loadPlugin(); this.workerRegistryClient.setRegistryStoppable(this); this.workerRegistryClient.start(); diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutor.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutor.java index 19421ee05e66..ea44f1790f7e 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutor.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutor.java @@ -21,7 +21,6 @@ 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.TaskPluginManager; import org.apache.dolphinscheduler.server.worker.config.WorkerConfig; import org.apache.dolphinscheduler.server.worker.registry.WorkerRegistryClient; import org.apache.dolphinscheduler.server.worker.rpc.WorkerMessageSender; @@ -35,13 +34,11 @@ public class DefaultWorkerTaskExecutor extends WorkerTaskExecutor { public DefaultWorkerTaskExecutor(@NonNull TaskExecutionContext taskExecutionContext, @NonNull WorkerConfig workerConfig, @NonNull WorkerMessageSender workerMessageSender, - @NonNull TaskPluginManager taskPluginManager, @Nullable StorageOperate storageOperate, @NonNull WorkerRegistryClient workerRegistryClient) { super(taskExecutionContext, workerConfig, workerMessageSender, - taskPluginManager, storageOperate, workerRegistryClient); } diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutorFactory.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutorFactory.java index 0141a5cd177b..20fa6a5e2afe 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutorFactory.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutorFactory.java @@ -19,7 +19,6 @@ import org.apache.dolphinscheduler.plugin.storage.api.StorageOperate; import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; -import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.server.worker.config.WorkerConfig; import org.apache.dolphinscheduler.server.worker.registry.WorkerRegistryClient; import org.apache.dolphinscheduler.server.worker.rpc.WorkerMessageSender; @@ -35,20 +34,17 @@ public class DefaultWorkerTaskExecutorFactory private final @NonNull TaskExecutionContext taskExecutionContext; private final @NonNull WorkerConfig workerConfig; private final @NonNull WorkerMessageSender workerMessageSender; - private final @NonNull TaskPluginManager taskPluginManager; private final @Nullable StorageOperate storageOperate; private final @NonNull WorkerRegistryClient workerRegistryClient; public DefaultWorkerTaskExecutorFactory(@NonNull TaskExecutionContext taskExecutionContext, @NonNull WorkerConfig workerConfig, @NonNull WorkerMessageSender workerMessageSender, - @NonNull TaskPluginManager taskPluginManager, @Nullable StorageOperate storageOperate, @NonNull WorkerRegistryClient workerRegistryClient) { this.taskExecutionContext = taskExecutionContext; this.workerConfig = workerConfig; this.workerMessageSender = workerMessageSender; - this.taskPluginManager = taskPluginManager; this.storageOperate = storageOperate; this.workerRegistryClient = workerRegistryClient; } @@ -59,7 +55,6 @@ public DefaultWorkerTaskExecutor createWorkerTaskExecutor() { taskExecutionContext, workerConfig, workerMessageSender, - taskPluginManager, storageOperate, workerRegistryClient); } diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutor.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutor.java index f713605a48ef..41cef6202862 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutor.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutor.java @@ -75,7 +75,6 @@ public abstract class WorkerTaskExecutor implements Runnable { protected final TaskExecutionContext taskExecutionContext; protected final WorkerConfig workerConfig; protected final WorkerMessageSender workerMessageSender; - protected final TaskPluginManager taskPluginManager; protected final @Nullable StorageOperate storageOperate; protected final WorkerRegistryClient workerRegistryClient; @@ -85,13 +84,11 @@ protected WorkerTaskExecutor( @NonNull TaskExecutionContext taskExecutionContext, @NonNull WorkerConfig workerConfig, @NonNull WorkerMessageSender workerMessageSender, - @NonNull TaskPluginManager taskPluginManager, @Nullable StorageOperate storageOperate, @NonNull WorkerRegistryClient workerRegistryClient) { this.taskExecutionContext = taskExecutionContext; this.workerConfig = workerConfig; this.workerMessageSender = workerMessageSender; - this.taskPluginManager = taskPluginManager; this.storageOperate = storageOperate; this.workerRegistryClient = workerRegistryClient; SensitiveDataConverter.addMaskPattern(K8S_CONFIG_REGEX); @@ -220,7 +217,7 @@ protected void beforeExecute() { log.info("WorkflowInstanceExecDir: {} check successfully", taskExecutionContext.getExecutePath()); TaskChannel taskChannel = - Optional.ofNullable(taskPluginManager.getTaskChannelMap().get(taskExecutionContext.getTaskType())) + Optional.ofNullable(TaskPluginManager.getTaskChannelMap().get(taskExecutionContext.getTaskType())) .orElseThrow(() -> new TaskPluginException(taskExecutionContext.getTaskType() + " task plugin not found, please check the task type is correct.")); diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutorFactoryBuilder.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutorFactoryBuilder.java index 599746818db7..56f320788426 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutorFactoryBuilder.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutorFactoryBuilder.java @@ -19,7 +19,6 @@ import org.apache.dolphinscheduler.plugin.storage.api.StorageOperate; import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; -import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.server.worker.config.WorkerConfig; import org.apache.dolphinscheduler.server.worker.registry.WorkerRegistryClient; import org.apache.dolphinscheduler.server.worker.rpc.WorkerMessageSender; @@ -36,12 +35,6 @@ public class WorkerTaskExecutorFactoryBuilder { @Autowired private WorkerMessageSender workerMessageSender; - @Autowired - private TaskPluginManager taskPluginManager; - - @Autowired - private WorkerTaskExecutorThreadPool workerManager; - @Autowired(required = false) private StorageOperate storageOperate; @@ -51,14 +44,11 @@ public class WorkerTaskExecutorFactoryBuilder { public WorkerTaskExecutorFactoryBuilder( WorkerConfig workerConfig, WorkerMessageSender workerMessageSender, - TaskPluginManager taskPluginManager, WorkerTaskExecutorThreadPool workerManager, StorageOperate storageOperate, WorkerRegistryClient workerRegistryClient) { this.workerConfig = workerConfig; this.workerMessageSender = workerMessageSender; - this.taskPluginManager = taskPluginManager; - this.workerManager = workerManager; this.storageOperate = storageOperate; this.workerRegistryClient = workerRegistryClient; } @@ -67,7 +57,6 @@ public WorkerTaskExecutorFactory createWorkerTaskE return new DefaultWorkerTaskExecutorFactory(taskExecutionContext, workerConfig, workerMessageSender, - taskPluginManager, storageOperate, workerRegistryClient); } diff --git a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutorTest.java b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutorTest.java index 43ef6f87d08e..e211fcdd1f79 100644 --- a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutorTest.java +++ b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/DefaultWorkerTaskExecutorTest.java @@ -20,7 +20,6 @@ import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.plugin.storage.api.StorageOperate; import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; -import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus; import org.apache.dolphinscheduler.server.worker.config.WorkerConfig; import org.apache.dolphinscheduler.server.worker.registry.WorkerRegistryClient; @@ -40,8 +39,6 @@ public class DefaultWorkerTaskExecutorTest { private WorkerMessageSender workerMessageSender = Mockito.mock(WorkerMessageSender.class); - private TaskPluginManager taskPluginManager = Mockito.mock(TaskPluginManager.class); - private StorageOperate storageOperate = Mockito.mock(StorageOperate.class); private WorkerRegistryClient workerRegistryClient = Mockito.mock(WorkerRegistryClient.class); @@ -58,7 +55,6 @@ public void testDryRun() { taskExecutionContext, workerConfig, workerMessageSender, - taskPluginManager, storageOperate, workerRegistryClient); @@ -82,7 +78,6 @@ public void testErrorboundTestDataSource() { taskExecutionContext, workerConfig, workerMessageSender, - taskPluginManager, storageOperate, workerRegistryClient); diff --git a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutorThreadPoolTest.java b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutorThreadPoolTest.java index 988f1f7fec4a..182ac6a1c258 100644 --- a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutorThreadPoolTest.java +++ b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/WorkerTaskExecutorThreadPoolTest.java @@ -22,7 +22,6 @@ import org.apache.dolphinscheduler.plugin.storage.api.StorageOperate; import org.apache.dolphinscheduler.plugin.task.api.TaskCallBack; import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; -import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.server.worker.config.TaskExecuteThreadsFullPolicy; import org.apache.dolphinscheduler.server.worker.config.WorkerConfig; import org.apache.dolphinscheduler.server.worker.registry.WorkerRegistryClient; @@ -68,7 +67,7 @@ static class MockWorkerTaskExecutor extends WorkerTaskExecutor { protected MockWorkerTaskExecutor(Runnable runnable) { super(TaskExecutionContext.builder().taskInstanceId((int) System.nanoTime()).build(), new WorkerConfig(), - new WorkerMessageSender(), new TaskPluginManager(), new StorageOperate() { + new WorkerMessageSender(), new StorageOperate() { @Override public void createTenantDirIfNotExists(String tenantCode) { diff --git a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/operator/TaskInstanceOperationFunctionTest.java b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/operator/TaskInstanceOperationFunctionTest.java index 592340214f21..f761dd61d6c6 100644 --- a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/operator/TaskInstanceOperationFunctionTest.java +++ b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/runner/operator/TaskInstanceOperationFunctionTest.java @@ -34,7 +34,6 @@ import org.apache.dolphinscheduler.plugin.storage.api.StorageOperate; import org.apache.dolphinscheduler.plugin.task.api.AbstractTask; import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; -import org.apache.dolphinscheduler.plugin.task.api.TaskPluginManager; import org.apache.dolphinscheduler.plugin.task.api.utils.LogUtils; import org.apache.dolphinscheduler.server.worker.config.WorkerConfig; import org.apache.dolphinscheduler.server.worker.message.MessageRetryRunner; @@ -70,8 +69,6 @@ public class TaskInstanceOperationFunctionTest { private WorkerMessageSender workerMessageSender = Mockito.mock(WorkerMessageSender.class); - private TaskPluginManager taskPluginManager = Mockito.mock(TaskPluginManager.class); - private WorkerTaskExecutorThreadPool workerManager = Mockito.mock(WorkerTaskExecutorThreadPool.class); private StorageOperate storageOperate = Mockito.mock(StorageOperate.class); @@ -94,7 +91,6 @@ public void testTaskInstanceOperationFunctionManager() { WorkerTaskExecutorFactoryBuilder workerTaskExecutorFactoryBuilder = new WorkerTaskExecutorFactoryBuilder( workerConfig, workerMessageSender, - taskPluginManager, workerManager, storageOperate, workerRegistryClient); @@ -189,7 +185,6 @@ public void testTaskInstanceDispatchOperationFunction() { WorkerTaskExecutorFactoryBuilder workerTaskExecutorFactoryBuilder = new WorkerTaskExecutorFactoryBuilder( workerConfig, workerMessageSender, - taskPluginManager, workerManager, storageOperate, workerRegistryClient); diff --git a/dolphinscheduler-worker/src/test/resources/logback.xml b/dolphinscheduler-worker/src/test/resources/logback.xml index d63ea4a0b533..10fc24d97434 100644 --- a/dolphinscheduler-worker/src/test/resources/logback.xml +++ b/dolphinscheduler-worker/src/test/resources/logback.xml @@ -22,7 +22,8 @@ - [%level] %date{yyyy-MM-dd HH:mm:ss.SSS Z} %logger{96}:[%line] - [WorkflowInstance-%X{workflowInstanceId:-0}][TaskInstance-%X{taskInstanceId:-0}] - %msg%n + [%level] %date{yyyy-MM-dd HH:mm:ss.SSS Z} %logger{96}:[%line] - + [WorkflowInstance-%X{workflowInstanceId:-0}][TaskInstance-%X{taskInstanceId:-0}] - %msg%n UTF-8 @@ -60,18 +61,15 @@ - [%level] %date{yyyy-MM-dd HH:mm:ss.SSS Z} %logger{96}:[%line] - [WorkflowInstance-%X{workflowInstanceId:-0}][TaskInstance-%X{taskInstanceId:-0}] - %msg%n + [%level] %date{yyyy-MM-dd HH:mm:ss.SSS Z} %logger{96}:[%line] - + [WorkflowInstance-%X{workflowInstanceId:-0}][TaskInstance-%X{taskInstanceId:-0}] - %msg%n UTF-8 - - - - - - + + From e66441a2c9ec38387b34f3712055af7308876efc Mon Sep 17 00:00:00 2001 From: privking <43061765+privking@users.noreply.github.com> Date: Sat, 20 Apr 2024 07:32:18 +0800 Subject: [PATCH 044/165] [FIX] Fix cannot recover a stopped workflow instance (#15880) --- .../workflow/instance/pause/recover/RecoverExecuteFunction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/executor/workflow/instance/pause/recover/RecoverExecuteFunction.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/executor/workflow/instance/pause/recover/RecoverExecuteFunction.java index 149e1abd29b7..34bd2561b146 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/executor/workflow/instance/pause/recover/RecoverExecuteFunction.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/executor/workflow/instance/pause/recover/RecoverExecuteFunction.java @@ -43,7 +43,7 @@ public RecoverExecuteFunction(CommandService commandService) { @Override public RecoverExecuteResult execute(RecoverExecuteRequest request) throws ExecuteRuntimeException { ProcessInstance workflowInstance = request.getWorkflowInstance(); - if (!workflowInstance.getState().isPause()) { + if (!(workflowInstance.getState().isPause() || workflowInstance.getState().isStop())) { throw new ExecuteRuntimeException( String.format("The workflow instance: %s state is %s, cannot recovery", workflowInstance.getName(), workflowInstance.getState())); From 6c78c8ec9a5a85a5792aaaae8d7c3e8fe47012c9 Mon Sep 17 00:00:00 2001 From: John Huang Date: Mon, 22 Apr 2024 15:05:59 +0800 Subject: [PATCH 045/165] [Improvement][Spark] Support Local Spark Cluster (#15589) * [Improvement][Spark] Support Local Spark Cluster * remote default local from deploy mode --------- Co-authored-by: Rick Cheng --- docs/docs/en/guide/task/spark.md | 1 + docs/docs/zh/guide/task/spark.md | 1 + .../plugin/task/spark/SparkParameters.java | 5 + .../plugin/task/spark/SparkTask.java | 23 +++-- .../task/spark/SparkParametersTest.java | 1 - .../plugin/task/spark/SparkTaskTest.java | 93 +++++++++++++++++-- .../src/locales/en_US/project.ts | 2 + .../src/locales/zh_CN/project.ts | 2 + .../task/components/node/fields/use-spark.ts | 26 ++++++ .../task/components/node/format-data.ts | 1 + .../task/components/node/tasks/use-spark.ts | 3 +- 11 files changed, 143 insertions(+), 15 deletions(-) diff --git a/docs/docs/en/guide/task/spark.md b/docs/docs/en/guide/task/spark.md index 3e0f83b253cf..930f2cd0b00b 100644 --- a/docs/docs/en/guide/task/spark.md +++ b/docs/docs/en/guide/task/spark.md @@ -24,6 +24,7 @@ Spark task type for executing Spark application. When executing the Spark task, |----------------------------|------------------------------------------------------------------------------------------------------------------------------------| | Program type | Supports Java, Scala, Python, and SQL. | | The class of main function | The **full path** of Main Class, the entry point of the Spark program. | +| Master | The The master URL for the cluster. | | Main jar package | The Spark jar package (upload by Resource Center). | | SQL scripts | SQL statements in .sql files that Spark sql runs. | | Deployment mode |

  • spark submit supports three modes: cluster, client and local.
  • spark sql supports client and local modes.
| diff --git a/docs/docs/zh/guide/task/spark.md b/docs/docs/zh/guide/task/spark.md index a392f5582694..2f7b2ee3469c 100644 --- a/docs/docs/zh/guide/task/spark.md +++ b/docs/docs/zh/guide/task/spark.md @@ -23,6 +23,7 @@ Spark 任务类型用于执行 Spark 应用。对于 Spark 节点,worker 支 - 程序类型:支持 Java、Scala、Python 和 SQL 四种语言。 - 主函数的 Class:Spark 程序的入口 Main class 的全路径。 - 主程序包:执行 Spark 程序的 jar 包(通过资源中心上传)。 +- Master:执行 Spark 集群的 Master Url。 - SQL脚本:Spark sql 运行的 .sql 文件中的 SQL 语句。 - 部署方式:(1) spark submit 支持 cluster、client 和 local 三种模式。 (2) spark sql 支持 client 和 local 两种模式。 diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/main/java/org/apache/dolphinscheduler/plugin/task/spark/SparkParameters.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/main/java/org/apache/dolphinscheduler/plugin/task/spark/SparkParameters.java index c5fcb5b76bf8..873ba22c71a3 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/main/java/org/apache/dolphinscheduler/plugin/task/spark/SparkParameters.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/main/java/org/apache/dolphinscheduler/plugin/task/spark/SparkParameters.java @@ -38,6 +38,11 @@ public class SparkParameters extends AbstractParameters { */ private String mainClass; + /** + * master url + */ + private String master; + /** * deploy mode local / cluster / client */ diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/main/java/org/apache/dolphinscheduler/plugin/task/spark/SparkTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/main/java/org/apache/dolphinscheduler/plugin/task/spark/SparkTask.java index a0d1f3fc77fa..3c5fc1769882 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/main/java/org/apache/dolphinscheduler/plugin/task/spark/SparkTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/main/java/org/apache/dolphinscheduler/plugin/task/spark/SparkTask.java @@ -124,22 +124,31 @@ protected Map getProperties() { */ private List populateSparkOptions() { List args = new ArrayList<>(); - args.add(SparkConstants.MASTER); + // see https://spark.apache.org/docs/latest/submitting-applications.html + // TODO remove the option 'local' from deploy-mode String deployMode = StringUtils.isNotEmpty(sparkParameters.getDeployMode()) ? sparkParameters.getDeployMode() : SparkConstants.DEPLOY_MODE_LOCAL; + boolean onLocal = SparkConstants.DEPLOY_MODE_LOCAL.equals(deployMode); boolean onNativeKubernetes = StringUtils.isNotEmpty(sparkParameters.getNamespace()); - String masterUrl = onNativeKubernetes ? SPARK_ON_K8S_MASTER_PREFIX + - Config.fromKubeconfig(taskExecutionContext.getK8sTaskExecutionContext().getConfigYaml()).getMasterUrl() - : SparkConstants.SPARK_ON_YARN; + String masterUrl = StringUtils.isNotEmpty(sparkParameters.getMaster()) ? sparkParameters.getMaster() + : onLocal ? deployMode + : onNativeKubernetes + ? SPARK_ON_K8S_MASTER_PREFIX + Config + .fromKubeconfig( + taskExecutionContext.getK8sTaskExecutionContext().getConfigYaml()) + .getMasterUrl() + : SparkConstants.SPARK_ON_YARN; + + args.add(SparkConstants.MASTER); + args.add(masterUrl); - if (!SparkConstants.DEPLOY_MODE_LOCAL.equals(deployMode)) { - args.add(masterUrl); + if (!onLocal) { args.add(SparkConstants.DEPLOY_MODE); + args.add(deployMode); } - args.add(deployMode); ProgramType programType = sparkParameters.getProgramType(); String mainClass = sparkParameters.getMainClass(); diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/test/java/org/apache/dolphinscheduler/plugin/task/spark/SparkParametersTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/test/java/org/apache/dolphinscheduler/plugin/task/spark/SparkParametersTest.java index ab164f2eb545..19ec707c62fe 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/test/java/org/apache/dolphinscheduler/plugin/task/spark/SparkParametersTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/test/java/org/apache/dolphinscheduler/plugin/task/spark/SparkParametersTest.java @@ -54,6 +54,5 @@ public void getResourceFilesList() { resourceFilesList = sparkParameters.getResourceFilesList(); Assertions.assertNotNull(resourceFilesList); Assertions.assertEquals(3, resourceFilesList.size()); - } } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/test/java/org/apache/dolphinscheduler/plugin/task/spark/SparkTaskTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/test/java/org/apache/dolphinscheduler/plugin/task/spark/SparkTaskTest.java index 78d8968e591a..513856256421 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/test/java/org/apache/dolphinscheduler/plugin/task/spark/SparkTaskTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-spark/src/test/java/org/apache/dolphinscheduler/plugin/task/spark/SparkTaskTest.java @@ -17,16 +17,27 @@ package org.apache.dolphinscheduler.plugin.task.spark; +import static org.apache.dolphinscheduler.plugin.task.spark.SparkConstants.TYPE_FILE; +import static org.mockito.ArgumentMatchers.any; + import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; import org.apache.dolphinscheduler.plugin.task.api.model.ResourceInfo; import org.apache.dolphinscheduler.plugin.task.api.resource.ResourceContext; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -41,25 +52,67 @@ public void testBuildCommandWithSparkSql() throws Exception { Mockito.when(taskExecutionContext.getExecutePath()).thenReturn("/tmp"); Mockito.when(taskExecutionContext.getTaskAppId()).thenReturn("5536"); + ResourceContext resourceContext = Mockito.mock(ResourceContext.class); + Mockito.when(taskExecutionContext.getResourceContext()).thenReturn(resourceContext); + ResourceContext.ResourceItem resourceItem = new ResourceContext.ResourceItem(); + resourceItem.setResourceAbsolutePathInLocal("test"); + Mockito.when(resourceContext.getResourceItem(any())).thenReturn(resourceItem); + + try (MockedStatic fileUtilsMockedStatic = Mockito.mockStatic(FileUtils.class)) { + fileUtilsMockedStatic + .when(() -> FileUtils + .readFileToString(any(File.class), any(Charset.class))) + .thenReturn("test"); + + SparkTask sparkTask = Mockito.spy(new SparkTask(taskExecutionContext)); + sparkTask.init(); + Assertions.assertEquals( + "${SPARK_HOME}/bin/spark-sql " + + "--master yarn " + + "--deploy-mode client " + + "--conf spark.driver.cores=1 " + + "--conf spark.driver.memory=512M " + + "--conf spark.executor.instances=2 " + + "--conf spark.executor.cores=2 " + + "--conf spark.executor.memory=1G " + + "--name sparksql " + + "-f /tmp/5536_node.sql", + sparkTask.getScript()); + } + } + + @Test + public void testBuildCommandWithSparkSubmit() { + String parameters = buildSparkParametersWithSparkSubmit(); + TaskExecutionContext taskExecutionContext = Mockito.mock(TaskExecutionContext.class); + ResourceContext.ResourceItem resourceItem = new ResourceContext.ResourceItem(); + resourceItem.setResourceAbsolutePathInStorage("/lib/dolphinscheduler-task-spark.jar"); + resourceItem.setResourceAbsolutePathInLocal("/lib/dolphinscheduler-task-spark.jar"); + ResourceContext resourceContext = new ResourceContext(); + resourceContext.addResourceItem(resourceItem); + + Mockito.when(taskExecutionContext.getTaskParams()).thenReturn(parameters); + Mockito.when(taskExecutionContext.getResourceContext()).thenReturn(resourceContext); SparkTask sparkTask = Mockito.spy(new SparkTask(taskExecutionContext)); sparkTask.init(); Assertions.assertEquals( - "${SPARK_HOME}/bin/spark-sql " + + "${SPARK_HOME}/bin/spark-submit " + "--master yarn " + "--deploy-mode client " + + "--class org.apache.dolphinscheduler.plugin.task.spark.SparkTaskTest " + "--conf spark.driver.cores=1 " + "--conf spark.driver.memory=512M " + "--conf spark.executor.instances=2 " + "--conf spark.executor.cores=2 " + "--conf spark.executor.memory=1G " + - "--name sparksql " + - "-f /tmp/5536_node.sql", + "--name spark " + + "/lib/dolphinscheduler-task-spark.jar", sparkTask.getScript()); } @Test - public void testBuildCommandWithSparkSubmit() { - String parameters = buildSparkParametersWithSparkSubmit(); + public void testBuildCommandWithSparkSubmitMaster() { + String parameters = buildSparkParametersWithMaster(); TaskExecutionContext taskExecutionContext = Mockito.mock(TaskExecutionContext.class); ResourceContext.ResourceItem resourceItem = new ResourceContext.ResourceItem(); resourceItem.setResourceAbsolutePathInStorage("/lib/dolphinscheduler-task-spark.jar"); @@ -73,7 +126,7 @@ public void testBuildCommandWithSparkSubmit() { sparkTask.init(); Assertions.assertEquals( "${SPARK_HOME}/bin/spark-submit " + - "--master yarn " + + "--master spark://localhost:7077 " + "--deploy-mode client " + "--class org.apache.dolphinscheduler.plugin.task.spark.SparkTaskTest " + "--conf spark.driver.cores=1 " + @@ -91,6 +144,7 @@ private String buildSparkParametersWithSparkSql() { sparkParameters.setLocalParams(Collections.emptyList()); sparkParameters.setRawScript("selcet 11111;"); sparkParameters.setProgramType(ProgramType.SQL); + sparkParameters.setSqlExecutionType(TYPE_FILE); sparkParameters.setMainClass(""); sparkParameters.setDeployMode("client"); sparkParameters.setAppName("sparksql"); @@ -100,6 +154,13 @@ private String buildSparkParametersWithSparkSql() { sparkParameters.setNumExecutors(2); sparkParameters.setExecutorMemory("1G"); sparkParameters.setExecutorCores(2); + + ResourceInfo resourceInfo1 = new ResourceInfo(); + resourceInfo1.setResourceName("testSparkParameters1.jar"); + List resourceInfos = new ArrayList<>(Arrays.asList( + resourceInfo1)); + sparkParameters.setResourceList(resourceInfos); + return JSONUtils.toJsonString(sparkParameters); } @@ -122,4 +183,24 @@ private String buildSparkParametersWithSparkSubmit() { return JSONUtils.toJsonString(sparkParameters); } + private String buildSparkParametersWithMaster() { + SparkParameters sparkParameters = new SparkParameters(); + sparkParameters.setLocalParams(Collections.emptyList()); + sparkParameters.setProgramType(ProgramType.SCALA); + sparkParameters.setMainClass("org.apache.dolphinscheduler.plugin.task.spark.SparkTaskTest"); + sparkParameters.setDeployMode("client"); + sparkParameters.setAppName("spark"); + sparkParameters.setMaster("spark://localhost:7077"); + sparkParameters.setOthers(""); + sparkParameters.setDriverCores(1); + sparkParameters.setDriverMemory("512M"); + sparkParameters.setNumExecutors(2); + sparkParameters.setExecutorMemory("1G"); + sparkParameters.setExecutorCores(2); + ResourceInfo resourceInfo = new ResourceInfo(); + resourceInfo.setResourceName("/lib/dolphinscheduler-task-spark.jar"); + sparkParameters.setMainJar(resourceInfo); + return JSONUtils.toJsonString(sparkParameters); + } + } diff --git a/dolphinscheduler-ui/src/locales/en_US/project.ts b/dolphinscheduler-ui/src/locales/en_US/project.ts index cb50b19fc737..7a3975252630 100644 --- a/dolphinscheduler-ui/src/locales/en_US/project.ts +++ b/dolphinscheduler-ui/src/locales/en_US/project.ts @@ -447,6 +447,8 @@ export default { timeout_period_tips: 'Timeout must be a positive integer', script: 'Script', script_tips: 'Please enter script(required)', + master: 'Master', + master_tips: 'Please enter master url(required)', init_script: 'Initialization script', init_script_tips: 'Please enter initialization script', resources: 'Resources', diff --git a/dolphinscheduler-ui/src/locales/zh_CN/project.ts b/dolphinscheduler-ui/src/locales/zh_CN/project.ts index 6865a49abc2f..50e0a821ef12 100644 --- a/dolphinscheduler-ui/src/locales/zh_CN/project.ts +++ b/dolphinscheduler-ui/src/locales/zh_CN/project.ts @@ -437,6 +437,8 @@ export default { timeout_period_tips: '超时时长必须为正整数', script: '脚本', script_tips: '请输入脚本(必填)', + master: 'Master', + master_tips: '请输入master url(必填)', init_script: '初始化脚本', init_script_tips: '请输入初始化脚本', resources: '资源', diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-spark.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-spark.ts index ad7fb77fa9a7..ab89e69e6d8f 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-spark.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-spark.ts @@ -37,6 +37,10 @@ export function useSpark(model: { [field: string]: any }): IJsonItem[] { model.programType === 'PYTHON' || model.programType === 'SQL' ? 0 : 24 ) + const masterSpan = computed(() => + model.programType === 'PYTHON' || model.programType === 'SQL' ? 0 : 24 + ) + const mainArgsSpan = computed(() => (model.programType === 'SQL' ? 0 : 24)) const rawScriptSpan = computed(() => @@ -138,6 +142,28 @@ export function useSpark(model: { [field: string]: any }): IJsonItem[] { message: t('project.node.script_tips') } }, + { + type: 'input', + field: 'master', + span: masterSpan, + name: t('project.node.master'), + props: { + placeholder: t('project.node.master_tips') + }, + validate: { + trigger: ['input', 'blur'], + required: false, + validator(validate: any, value: string) { + if ( + model.programType !== 'PYTHON' && + !value && + model.programType !== 'SQL' + ) { + return new Error(t('project.node.master_tips')) + } + } + } + }, useDeployMode(24, ref(true), showCluster), useNamespace(), { 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 ef9e5dec610b..2ab1712b6f07 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 @@ -68,6 +68,7 @@ export function formatParams(data: INodeData): { } if (data.taskType === 'SPARK') { + taskParams.master = data.master taskParams.driverCores = data.driverCores taskParams.driverMemory = data.driverMemory taskParams.numExecutors = data.numExecutors diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-spark.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-spark.ts index 05aa0fe1c601..1e5c929b0f4e 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-spark.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-spark.ts @@ -45,7 +45,8 @@ export function useSpark({ timeout: 30, programType: 'SCALA', rawScript: '', - deployMode: 'local', + master: '', + deployMode: '', driverCores: 1, driverMemory: '512M', numExecutors: 2, From e2c8b080f90c71aa6045609a84ffde9272892fea Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Mon, 22 Apr 2024 17:05:24 +0800 Subject: [PATCH 046/165] [DSIP-31] Reduce the thread pool size of hikari (#15890) --- .../src/main/resources/application.yaml | 8 -------- dolphinscheduler-api/src/main/resources/application.yaml | 8 -------- .../src/main/resources/application.yaml | 8 -------- .../src/main/resources/application.yaml | 8 -------- .../src/test/resources/application-mysql.yaml | 8 -------- .../src/test/resources/application-postgresql.yaml | 8 -------- 6 files changed, 48 deletions(-) diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/resources/application.yaml b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/resources/application.yaml index 0b28d6c8b053..0dbb6988ce28 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/resources/application.yaml +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/resources/application.yaml @@ -30,15 +30,7 @@ spring: password: root hikari: connection-test-query: select 1 - minimum-idle: 5 - auto-commit: true - validation-timeout: 3000 pool-name: DolphinScheduler - maximum-pool-size: 50 - connection-timeout: 30000 - idle-timeout: 600000 - leak-detection-threshold: 0 - initialization-fail-timeout: 1 # Mybatis-plus configuration, you don't need to change it mybatis-plus: diff --git a/dolphinscheduler-api/src/main/resources/application.yaml b/dolphinscheduler-api/src/main/resources/application.yaml index 61f9c8a592d1..e38d0c5a8e86 100644 --- a/dolphinscheduler-api/src/main/resources/application.yaml +++ b/dolphinscheduler-api/src/main/resources/application.yaml @@ -51,15 +51,7 @@ spring: password: root hikari: connection-test-query: select 1 - minimum-idle: 5 - auto-commit: true - validation-timeout: 3000 pool-name: DolphinScheduler - maximum-pool-size: 50 - connection-timeout: 30000 - idle-timeout: 600000 - leak-detection-threshold: 0 - initialization-fail-timeout: 1 quartz: auto-startup: false job-store-type: jdbc diff --git a/dolphinscheduler-master/src/main/resources/application.yaml b/dolphinscheduler-master/src/main/resources/application.yaml index c2d8f5e787cf..f18c6ef61db3 100644 --- a/dolphinscheduler-master/src/main/resources/application.yaml +++ b/dolphinscheduler-master/src/main/resources/application.yaml @@ -29,15 +29,7 @@ spring: password: root hikari: connection-test-query: select 1 - minimum-idle: 5 - auto-commit: true - validation-timeout: 3000 pool-name: DolphinScheduler - maximum-pool-size: 50 - connection-timeout: 30000 - idle-timeout: 600000 - leak-detection-threshold: 0 - initialization-fail-timeout: 1 quartz: job-store-type: jdbc jdbc: diff --git a/dolphinscheduler-tools/src/main/resources/application.yaml b/dolphinscheduler-tools/src/main/resources/application.yaml index 136a4c5fd4c4..23680728dcda 100644 --- a/dolphinscheduler-tools/src/main/resources/application.yaml +++ b/dolphinscheduler-tools/src/main/resources/application.yaml @@ -24,15 +24,7 @@ spring: password: root hikari: connection-test-query: select 1 - minimum-idle: 5 - auto-commit: true - validation-timeout: 3000 pool-name: DolphinScheduler - maximum-pool-size: 50 - connection-timeout: 30000 - idle-timeout: 600000 - leak-detection-threshold: 0 - initialization-fail-timeout: 1 # Mybatis-plus configuration, you don't need to change it mybatis-plus: diff --git a/dolphinscheduler-tools/src/test/resources/application-mysql.yaml b/dolphinscheduler-tools/src/test/resources/application-mysql.yaml index 0b553c2bda68..d33a68a7f8ea 100644 --- a/dolphinscheduler-tools/src/test/resources/application-mysql.yaml +++ b/dolphinscheduler-tools/src/test/resources/application-mysql.yaml @@ -24,15 +24,7 @@ spring: password: root hikari: connection-test-query: select 1 - minimum-idle: 5 - auto-commit: true - validation-timeout: 3000 pool-name: DolphinScheduler - maximum-pool-size: 50 - connection-timeout: 30000 - idle-timeout: 600000 - leak-detection-threshold: 0 - initialization-fail-timeout: 1 mybatis-plus: mapper-locations: classpath:org/apache/dolphinscheduler/dao/mapper/*Mapper.xml diff --git a/dolphinscheduler-tools/src/test/resources/application-postgresql.yaml b/dolphinscheduler-tools/src/test/resources/application-postgresql.yaml index 21daaa0606a6..a8be29014f14 100644 --- a/dolphinscheduler-tools/src/test/resources/application-postgresql.yaml +++ b/dolphinscheduler-tools/src/test/resources/application-postgresql.yaml @@ -24,15 +24,7 @@ spring: password: root hikari: connection-test-query: select 1 - minimum-idle: 5 - auto-commit: true - validation-timeout: 3000 pool-name: DolphinScheduler - maximum-pool-size: 50 - connection-timeout: 30000 - idle-timeout: 600000 - leak-detection-threshold: 0 - initialization-fail-timeout: 1 mybatis-plus: mapper-locations: classpath:org/apache/dolphinscheduler/dao/mapper/*Mapper.xml From e9d85914d78197314bee585500d94a6a90733789 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Mon, 22 Apr 2024 17:41:17 +0800 Subject: [PATCH 047/165] Fix queryByTypeAndJobId might error due to multiple result (#15883) --- .../dao/mapper/TriggerRelationMapper.java | 2 +- .../dao/mapper/TriggerRelationMapperTest.java | 26 ++++----- dolphinscheduler-dist/release-docs/LICENSE | 2 +- .../process/TriggerRelationService.java | 4 +- .../process/TriggerRelationServiceImpl.java | 41 ++++++++++---- .../process/TriggerRelationServiceTest.java | 56 ++++++++----------- pom.xml | 7 +++ tools/dependencies/known-dependencies.txt | 4 +- 8 files changed, 79 insertions(+), 63 deletions(-) diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/TriggerRelationMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/TriggerRelationMapper.java index 10a0acf47fbb..912ef2810166 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/TriggerRelationMapper.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/TriggerRelationMapper.java @@ -36,7 +36,7 @@ public interface TriggerRelationMapper extends BaseMapper { * @param jobId * @return */ - TriggerRelation queryByTypeAndJobId(@Param("triggerType") Integer triggerType, @Param("jobId") int jobId); + List queryByTypeAndJobId(@Param("triggerType") Integer triggerType, @Param("jobId") int jobId); /** * query triggerRelation by code diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/TriggerRelationMapperTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/TriggerRelationMapperTest.java index d3f4fcc666bf..7e31b64a23f2 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/TriggerRelationMapperTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/TriggerRelationMapperTest.java @@ -17,13 +17,13 @@ package org.apache.dolphinscheduler.dao.mapper; +import static com.google.common.truth.Truth.assertThat; + import org.apache.dolphinscheduler.common.enums.ApiTriggerType; import org.apache.dolphinscheduler.common.utils.DateUtils; import org.apache.dolphinscheduler.dao.BaseDaoTest; import org.apache.dolphinscheduler.dao.entity.TriggerRelation; -import java.util.List; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -67,9 +67,9 @@ public void testSelectById() { @Test public void testQueryByTypeAndJobId() { TriggerRelation expectRelation = createTriggerRelation(); - TriggerRelation actualRelation = triggerRelationMapper.queryByTypeAndJobId( - expectRelation.getTriggerType(), expectRelation.getJobId()); - Assertions.assertEquals(expectRelation, actualRelation); + assertThat( + triggerRelationMapper.queryByTypeAndJobId(expectRelation.getTriggerType(), expectRelation.getJobId())) + .containsExactly(expectRelation); } /** @@ -80,9 +80,8 @@ public void testQueryByTypeAndJobId() { @Test public void testQueryByTriggerRelationCode() { TriggerRelation expectRelation = createTriggerRelation(); - List actualRelations = triggerRelationMapper.queryByTriggerRelationCode( - expectRelation.getTriggerCode()); - Assertions.assertEquals(actualRelations.size(), 1); + assertThat(triggerRelationMapper.queryByTriggerRelationCode(expectRelation.getTriggerCode())) + .containsExactly(expectRelation); } /** @@ -93,17 +92,15 @@ public void testQueryByTriggerRelationCode() { @Test public void testQueryByTriggerRelationCodeAndType() { TriggerRelation expectRelation = createTriggerRelation(); - List actualRelations = triggerRelationMapper.queryByTriggerRelationCodeAndType( - expectRelation.getTriggerCode(), expectRelation.getTriggerType()); - Assertions.assertEquals(actualRelations.size(), 1); + assertThat(triggerRelationMapper.queryByTriggerRelationCodeAndType(expectRelation.getTriggerCode(), + expectRelation.getTriggerType())).containsExactly(expectRelation); } @Test public void testUpsert() { TriggerRelation expectRelation = createTriggerRelation(); triggerRelationMapper.upsert(expectRelation); - TriggerRelation actualRelation = triggerRelationMapper.selectById(expectRelation.getId()); - Assertions.assertEquals(expectRelation, actualRelation); + assertThat(triggerRelationMapper.selectById(expectRelation.getId())).isEqualTo(expectRelation); } /** @@ -113,8 +110,7 @@ public void testUpsert() { public void testDelete() { TriggerRelation expectRelation = createTriggerRelation(); triggerRelationMapper.deleteById(expectRelation.getId()); - TriggerRelation actualRelation = triggerRelationMapper.selectById(expectRelation.getId()); - Assertions.assertNull(actualRelation); + assertThat(triggerRelationMapper.selectById(expectRelation.getId())).isNull(); } /** diff --git a/dolphinscheduler-dist/release-docs/LICENSE b/dolphinscheduler-dist/release-docs/LICENSE index 8856138aa4b5..84832b2b3238 100644 --- a/dolphinscheduler-dist/release-docs/LICENSE +++ b/dolphinscheduler-dist/release-docs/LICENSE @@ -528,7 +528,7 @@ The text of each license is also included at licenses/LICENSE-[project].txt. nimbus-jose-jwt 9.22: https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt/9.22, Apache 2.0 woodstox-core 6.4.0: https://mvnrepository.com/artifact/com.fasterxml.woodstox/woodstox-core/6.4.0, Apache 2.0 auto-value 1.10.1: https://mvnrepository.com/artifact/com.google.auto.value/auto-value/1.10.1, Apache 2.0 - auto-value-annotations 1.10.1: https://mvnrepository.com/artifact/com.google.auto.value/auto-value-annotations/1.10.1, Apache 2.0 + auto-value-annotations 1.10.4: https://mvnrepository.com/artifact/com.google.auto.value/auto-value-annotations/1.10.4, Apache 2.0 conscrypt-openjdk-uber 2.5.2: https://mvnrepository.com/artifact/org.conscrypt/conscrypt-openjdk-uber/2.5.2, Apache 2.0 gapic-google-cloud-storage-v2 2.18.0-alpha: https://mvnrepository.com/artifact/com.google.api.grpc/gapic-google-cloud-storage-v2/2.18.0-alpha, Apache 2.0 google-api-client 2.2.0: https://mvnrepository.com/artifact/com.google.api-client/google-api-client/2.2.0, Apache 2.0 diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/TriggerRelationService.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/TriggerRelationService.java index b78f4779313d..6de2506afd18 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/TriggerRelationService.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/TriggerRelationService.java @@ -20,6 +20,8 @@ import org.apache.dolphinscheduler.common.enums.ApiTriggerType; import org.apache.dolphinscheduler.dao.entity.TriggerRelation; +import java.util.List; + import org.springframework.stereotype.Component; /** @@ -30,7 +32,7 @@ public interface TriggerRelationService { void saveTriggerToDb(ApiTriggerType type, Long triggerCode, Integer jobId); - TriggerRelation queryByTypeAndJobId(ApiTriggerType apiTriggerType, int jobId); + List queryByTypeAndJobId(ApiTriggerType apiTriggerType, int jobId); int saveCommandTrigger(Integer commandId, Integer processInstanceId); diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/TriggerRelationServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/TriggerRelationServiceImpl.java index df41c41eef2f..0344ea87ea01 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/TriggerRelationServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/process/TriggerRelationServiceImpl.java @@ -21,14 +21,20 @@ import org.apache.dolphinscheduler.dao.entity.TriggerRelation; import org.apache.dolphinscheduler.dao.mapper.TriggerRelationMapper; +import org.apache.commons.collections4.CollectionUtils; + import java.util.Date; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** - * Trigger relation operator to db + * Trigger relation operator to db */ +@Slf4j @Component public class TriggerRelationServiceImpl implements TriggerRelationService { @@ -45,29 +51,44 @@ public void saveTriggerToDb(ApiTriggerType type, Long triggerCode, Integer jobId triggerRelation.setUpdateTime(new Date()); triggerRelationMapper.upsert(triggerRelation); } + @Override - public TriggerRelation queryByTypeAndJobId(ApiTriggerType apiTriggerType, int jobId) { + public List queryByTypeAndJobId(ApiTriggerType apiTriggerType, int jobId) { return triggerRelationMapper.queryByTypeAndJobId(apiTriggerType.getCode(), jobId); } @Override public int saveCommandTrigger(Integer commandId, Integer processInstanceId) { - TriggerRelation exist = queryByTypeAndJobId(ApiTriggerType.PROCESS, processInstanceId); - if (exist == null) { + List existTriggers = queryByTypeAndJobId(ApiTriggerType.PROCESS, processInstanceId); + if (CollectionUtils.isEmpty(existTriggers)) { return 0; } - saveTriggerToDb(ApiTriggerType.COMMAND, exist.getTriggerCode(), commandId); - return 1; + existTriggers.forEach(triggerRelation -> saveTriggerToDb(ApiTriggerType.COMMAND, + triggerRelation.getTriggerCode(), commandId)); + int triggerRelationSize = existTriggers.size(); + if (triggerRelationSize > 1) { + // Fix https://github.com/apache/dolphinscheduler/issues/15864 + // This case shouldn't happen + log.error("The PROCESS TriggerRelation of command: {} is more than one", commandId); + } + return existTriggers.size(); } @Override public int saveProcessInstanceTrigger(Integer commandId, Integer processInstanceId) { - TriggerRelation exist = queryByTypeAndJobId(ApiTriggerType.COMMAND, commandId); - if (exist == null) { + List existTriggers = queryByTypeAndJobId(ApiTriggerType.COMMAND, commandId); + if (CollectionUtils.isEmpty(existTriggers)) { return 0; } - saveTriggerToDb(ApiTriggerType.PROCESS, exist.getTriggerCode(), processInstanceId); - return 1; + existTriggers.forEach(triggerRelation -> saveTriggerToDb(ApiTriggerType.PROCESS, + triggerRelation.getTriggerCode(), processInstanceId)); + int triggerRelationSize = existTriggers.size(); + if (triggerRelationSize > 1) { + // Fix https://github.com/apache/dolphinscheduler/issues/15864 + // This case shouldn't happen + log.error("The COMMAND TriggerRelation of command: {} is more than one", commandId); + } + return existTriggers.size(); } } diff --git a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/process/TriggerRelationServiceTest.java b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/process/TriggerRelationServiceTest.java index 8f4790111cdc..4f3bbb0bc788 100644 --- a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/process/TriggerRelationServiceTest.java +++ b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/process/TriggerRelationServiceTest.java @@ -17,24 +17,26 @@ package org.apache.dolphinscheduler.service.process; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + import org.apache.dolphinscheduler.common.enums.ApiTriggerType; import org.apache.dolphinscheduler.dao.entity.TriggerRelation; import org.apache.dolphinscheduler.dao.mapper.TriggerRelationMapper; -import org.apache.dolphinscheduler.service.cron.CronUtilsTest; import java.util.Date; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + +import com.google.common.collect.Lists; /** * Trigger Relation Service Test @@ -43,8 +45,6 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class TriggerRelationServiceTest { - private static final Logger logger = LoggerFactory.getLogger(CronUtilsTest.class); - @InjectMocks private TriggerRelationServiceImpl triggerRelationService; @Mock @@ -52,47 +52,37 @@ public class TriggerRelationServiceTest { @Test public void saveTriggerToDb() { - Mockito.doNothing().when(triggerRelationMapper).upsert(Mockito.any()); + doNothing().when(triggerRelationMapper).upsert(any()); triggerRelationService.saveTriggerToDb(ApiTriggerType.COMMAND, 1234567890L, 100); } @Test public void queryByTypeAndJobId() { - Mockito.doNothing().when(triggerRelationMapper).upsert(Mockito.any()); - Mockito.when(triggerRelationMapper.queryByTypeAndJobId(ApiTriggerType.PROCESS.getCode(), 100)) - .thenReturn(getTriggerTdoDb()); + doNothing().when(triggerRelationMapper).upsert(any()); + when(triggerRelationMapper.queryByTypeAndJobId(ApiTriggerType.PROCESS.getCode(), 100)) + .thenReturn(Lists.newArrayList(getTriggerTdoDb())); - TriggerRelation triggerRelation1 = triggerRelationService.queryByTypeAndJobId( - ApiTriggerType.PROCESS, 100); - Assertions.assertNotNull(triggerRelation1); - TriggerRelation triggerRelation2 = triggerRelationService.queryByTypeAndJobId( - ApiTriggerType.PROCESS, 200); - Assertions.assertNull(triggerRelation2); + assertThat(triggerRelationService.queryByTypeAndJobId(ApiTriggerType.PROCESS, 100)).hasSize(1); + assertThat(triggerRelationService.queryByTypeAndJobId(ApiTriggerType.PROCESS, 200)).isEmpty(); } @Test public void saveCommandTrigger() { - Mockito.doNothing().when(triggerRelationMapper).upsert(Mockito.any()); - Mockito.when(triggerRelationMapper.queryByTypeAndJobId(ApiTriggerType.PROCESS.getCode(), 100)) - .thenReturn(getTriggerTdoDb()); - int result = -1; - result = triggerRelationService.saveCommandTrigger(1234567890, 100); - Assertions.assertTrue(result > 0); - result = triggerRelationService.saveCommandTrigger(1234567890, 200); - Assertions.assertTrue(result == 0); + doNothing().when(triggerRelationMapper).upsert(any()); + when(triggerRelationMapper.queryByTypeAndJobId(ApiTriggerType.PROCESS.getCode(), 100)) + .thenReturn(Lists.newArrayList(getTriggerTdoDb())); + assertThat(triggerRelationService.saveCommandTrigger(1234567890, 100)).isAtLeast(1); + assertThat(triggerRelationService.saveCommandTrigger(1234567890, 200)).isEqualTo(0); } @Test public void saveProcessInstanceTrigger() { - Mockito.doNothing().when(triggerRelationMapper).upsert(Mockito.any()); - Mockito.when(triggerRelationMapper.queryByTypeAndJobId(ApiTriggerType.COMMAND.getCode(), 100)) - .thenReturn(getTriggerTdoDb()); - int result = -1; - result = triggerRelationService.saveProcessInstanceTrigger(100, 1234567890); - Assertions.assertTrue(result > 0); - result = triggerRelationService.saveProcessInstanceTrigger(200, 1234567890); - Assertions.assertTrue(result == 0); + doNothing().when(triggerRelationMapper).upsert(any()); + when(triggerRelationMapper.queryByTypeAndJobId(ApiTriggerType.COMMAND.getCode(), 100)) + .thenReturn(Lists.newArrayList(getTriggerTdoDb())); + assertThat(triggerRelationService.saveProcessInstanceTrigger(100, 1234567890)).isAtLeast(1); + assertThat(triggerRelationService.saveProcessInstanceTrigger(200, 1234567890)).isEqualTo(0); } private TriggerRelation getTriggerTdoDb() { diff --git a/pom.xml b/pom.xml index 4a71741f8e34..dd4cb1adf19c 100755 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,7 @@ 7.1.2 1.18.20 4.2.0 + 1.4.2 apache ${project.name} ${project.version} @@ -372,6 +373,12 @@ ${awaitility.version} test + + com.google.truth + truth + ${truth.version} + test + diff --git a/tools/dependencies/known-dependencies.txt b/tools/dependencies/known-dependencies.txt index 6c2e89fb37a1..33987119db02 100644 --- a/tools/dependencies/known-dependencies.txt +++ b/tools/dependencies/known-dependencies.txt @@ -9,7 +9,7 @@ animal-sniffer-annotations-1.19.jar annotations-2.17.282.jar annotations-13.0.jar apache-client-2.17.282.jar -asm-9.1.jar +asm-9.6.jar aspectjweaver-1.9.7.jar aspectjrt-1.9.7.jar auth-2.17.282.jar @@ -434,7 +434,7 @@ woodstox-core-6.4.0.jar azure-core-management-1.10.1.jar api-common-2.6.0.jar auto-value-1.10.1.jar -auto-value-annotations-1.10.1.jar +auto-value-annotations-1.10.4.jar bcpkix-jdk15on-1.67.jar bcprov-jdk15on-1.67.jar conscrypt-openjdk-uber-2.5.2.jar From 59f060e278ad0c2400e4dea6359ffda8b1f71d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=BA=E9=98=B3?= Date: Tue, 23 Apr 2024 15:10:08 +0800 Subject: [PATCH 048/165] [Improvement] Fix alert code smell --- .../plugin/alert/dingtalk/DingTalkSender.java | 28 ++++++------------- .../alert/dingtalk/DingTalkSenderTest.java | 3 +- .../plugin/alert/email/EmailConstants.java | 2 -- .../plugin/alert/email/MailSender.java | 6 ++-- .../plugin/alert/email/ExcelUtilsTest.java | 11 ++++++-- .../plugin/alert/email/MailUtilsTest.java | 22 +++++++-------- .../plugin/alert/feishu/FeiShuSender.java | 3 +- .../plugin/alert/http/HttpSender.java | 9 +++--- .../prometheus/PrometheusAlertSender.java | 3 +- .../plugin/alert/script/ProcessUtilsTest.java | 3 +- .../plugin/alert/slack/SlackSender.java | 5 ++-- .../plugin/alert/telegram/TelegramSender.java | 2 +- .../alert/wechat/WeChatAlertConstants.java | 2 -- .../plugin/alert/wechat/WeChatSender.java | 27 ++++++------------ .../common/constants/Constants.java | 5 ---- .../common/utils/FileUtils.java | 3 +- .../common/utils/HttpUtils.java | 3 +- 17 files changed, 60 insertions(+), 77 deletions(-) diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSender.java index c0070ac11c1e..c8ded8cfadcf 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSender.java @@ -48,6 +48,8 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** @@ -189,7 +191,7 @@ private String sendMsg(String title, String content) throws IOException { String resp; try { HttpEntity entity = response.getEntity(); - resp = EntityUtils.toString(entity, "UTF-8"); + resp = EntityUtils.toString(entity, StandardCharsets.UTF_8); EntityUtils.consume(entity); } finally { response.close(); @@ -317,15 +319,17 @@ private String generateSignedUrl() { String sign = org.apache.commons.lang3.StringUtils.EMPTY; try { Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256")); - byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8")); - sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8"); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); + sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)), StandardCharsets.UTF_8.name()); } catch (Exception e) { log.error("generate sign error, message:{}", e); } return url + "×tamp=" + timestamp + "&sign=" + sign; } + @Getter + @Setter static final class DingTalkSendMsgResponse { private Integer errcode; @@ -334,22 +338,6 @@ static final class DingTalkSendMsgResponse { public DingTalkSendMsgResponse() { } - public Integer getErrcode() { - return this.errcode; - } - - public void setErrcode(Integer errcode) { - this.errcode = errcode; - } - - public String getErrmsg() { - return this.errmsg; - } - - public void setErrmsg(String errmsg) { - this.errmsg = errmsg; - } - @Override public boolean equals(final Object o) { if (o == this) { diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSenderTest.java index 7a8df0549cb4..cd30105c7a33 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSenderTest.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.alert.api.AlertResult; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -47,7 +48,7 @@ public void initDingTalkConfig() { @Test public void testSend() { DingTalkSender dingTalkSender = new DingTalkSender(dingTalkConfig); - dingTalkSender.sendDingTalkMsg("keyWord+Welcome", "UTF-8"); + dingTalkSender.sendDingTalkMsg("keyWord+Welcome", StandardCharsets.UTF_8.name()); dingTalkConfig.put(DingTalkParamsConstants.NAME_DING_TALK_PROXY_ENABLE, "true"); dingTalkSender = new DingTalkSender(dingTalkConfig); AlertResult alertResult = dingTalkSender.sendDingTalkMsg("title", "content test"); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/EmailConstants.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/EmailConstants.java index 94eb4efa39bd..3eec7022fdd8 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/EmailConstants.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/EmailConstants.java @@ -56,8 +56,6 @@ public final class EmailConstants { public static final String TABLE_BODY_HTML_TAIL = ""; - public static final String UTF_8 = "UTF-8"; - public static final String EXCEL_SUFFIX_XLSX = ".xlsx"; public static final String SINGLE_SLASH = "/"; diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/MailSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/MailSender.java index 2e400efbceb7..8826a44fbac7 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/MailSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/MailSender.java @@ -34,6 +34,7 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -171,7 +172,7 @@ public AlertResult sendMails(List receivers, List receiverCcs, S Session session = getSession(); email.setMailSession(session); email.setFrom(mailSenderEmail); - email.setCharset(EmailConstants.UTF_8); + email.setCharset(StandardCharsets.UTF_8.name()); if (CollectionUtils.isNotEmpty(receivers)) { // receivers mail for (String receiver : receivers) { @@ -344,7 +345,8 @@ private void attachContent(String title, String content, String partContent, ExcelUtils.genExcelFile(content, randomFilename, xlsFilePath); part2.attachFile(file); - part2.setFileName(MimeUtility.encodeText(title + EmailConstants.EXCEL_SUFFIX_XLSX, EmailConstants.UTF_8, "B")); + part2.setFileName( + MimeUtility.encodeText(title + EmailConstants.EXCEL_SUFFIX_XLSX, StandardCharsets.UTF_8.name(), "B")); // add components to collection partList.addBodyPart(part1); partList.addBodyPart(part2); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/ExcelUtilsTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/ExcelUtilsTest.java index f428a16d9228..f28df77de971 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/ExcelUtilsTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/ExcelUtilsTest.java @@ -66,8 +66,15 @@ public void testGenExcelFile() { @Test public void testGenExcelFileByCheckDir() { - ExcelUtils.genExcelFile("[{\"a\": \"a\"},{\"a\": \"a\"}]", "t", "/tmp/xls"); - File file = new File("/tmp/xls" + EmailConstants.SINGLE_SLASH + "t" + EmailConstants.EXCEL_SUFFIX_XLSX); + String path = "/tmp/xls"; + ExcelUtils.genExcelFile("[{\"a\": \"a\"},{\"a\": \"a\"}]", "t", path); + File file = + new File( + path + + EmailConstants.SINGLE_SLASH + + "t" + + EmailConstants.EXCEL_SUFFIX_XLSX); file.delete(); + Assertions.assertFalse(file.exists()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/MailUtilsTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/MailUtilsTest.java index f562999be02e..acc255ae0e9a 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/MailUtilsTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/MailUtilsTest.java @@ -18,10 +18,9 @@ package org.apache.dolphinscheduler.plugin.alert.email; import org.apache.dolphinscheduler.alert.api.AlertConstants; +import org.apache.dolphinscheduler.alert.api.AlertResult; import org.apache.dolphinscheduler.alert.api.ShowType; import org.apache.dolphinscheduler.common.utils.JSONUtils; -import org.apache.dolphinscheduler.plugin.alert.email.template.AlertTemplate; -import org.apache.dolphinscheduler.plugin.alert.email.template.DefaultHTMLTemplate; import java.util.ArrayList; import java.util.HashMap; @@ -42,7 +41,6 @@ public class MailUtilsTest { private static final Logger logger = LoggerFactory.getLogger(MailUtilsTest.class); static MailSender mailSender; private static Map emailConfig = new HashMap<>(); - private static AlertTemplate alertTemplate; @BeforeAll public static void initEmailConfig() { @@ -59,7 +57,6 @@ public static void initEmailConfig() { emailConfig.put(MailParamsConstants.NAME_PLUGIN_DEFAULT_EMAIL_RECEIVERS, "347801120@qq.com"); emailConfig.put(MailParamsConstants.NAME_PLUGIN_DEFAULT_EMAIL_RECEIVERCCS, "347801120@qq.com"); emailConfig.put(AlertConstants.NAME_SHOW_TYPE, ShowType.TEXT.getDescp()); - alertTemplate = new DefaultHTMLTemplate(); mailSender = new MailSender(emailConfig); } @@ -77,9 +74,10 @@ public void testSendMails() { + "\"Host: 192.168.xx.xx\"," + "\"Notify group :4\"]"; - mailSender.sendMails( + AlertResult alertResult = mailSender.sendMails( "Mysql Exception", content); + Assertions.assertEquals("false", alertResult.getStatus()); } @Test @@ -108,7 +106,8 @@ void testAuthCheck() { emailConfig.put(MailParamsConstants.NAME_MAIL_USER, "user"); emailConfig.put(MailParamsConstants.NAME_MAIL_PASSWD, "passwd"); mailSender = new MailSender(emailConfig); - mailSender.sendMails(title, content); + AlertResult alertResult = mailSender.sendMails(title, content); + Assertions.assertEquals("false", alertResult.getStatus()); } public String list2String() { @@ -134,7 +133,6 @@ public String list2String() { logger.info(mapjson); return mapjson; - } @Test @@ -143,7 +141,8 @@ public void testSendTableMail() { String content = list2String(); emailConfig.put(AlertConstants.NAME_SHOW_TYPE, ShowType.TABLE.getDescp()); mailSender = new MailSender(emailConfig); - mailSender.sendMails(title, content); + AlertResult alertResult = mailSender.sendMails(title, content); + Assertions.assertEquals("false", alertResult.getStatus()); } @Test @@ -151,7 +150,8 @@ public void testAttachmentFile() throws Exception { String content = list2String(); emailConfig.put(AlertConstants.NAME_SHOW_TYPE, ShowType.ATTACHMENT.getDescp()); mailSender = new MailSender(emailConfig); - mailSender.sendMails("gaojing", content); + AlertResult alertResult = mailSender.sendMails("gaojing", content); + Assertions.assertEquals("false", alertResult.getStatus()); } @Test @@ -159,7 +159,7 @@ public void testTableAttachmentFile() throws Exception { String content = list2String(); emailConfig.put(AlertConstants.NAME_SHOW_TYPE, ShowType.TABLE_ATTACHMENT.getDescp()); mailSender = new MailSender(emailConfig); - mailSender.sendMails("gaojing", content); + AlertResult alertResult = mailSender.sendMails("gaojing", content); + Assertions.assertEquals("false", alertResult.getStatus()); } - } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSender.java index 14a1d63ff08c..369060843c1e 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSender.java @@ -31,6 +31,7 @@ import org.apache.http.util.EntityUtils; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -161,7 +162,7 @@ private String sendMsg(AlertData alertData) throws IOException { String resp; try { HttpEntity entity = response.getEntity(); - resp = EntityUtils.toString(entity, "utf-8"); + resp = EntityUtils.toString(entity, StandardCharsets.UTF_8); EntityUtils.consume(entity); } finally { response.close(); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSender.java index 999a0c9599bd..a1de852407ca 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSender.java @@ -39,6 +39,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -58,7 +59,6 @@ public final class HttpSender { * request type get */ private static final String REQUEST_TYPE_GET = "GET"; - private static final String DEFAULT_CHARSET = "utf-8"; private final String headerParams; private final String bodyParams; private final String contentField; @@ -124,7 +124,7 @@ public String getResponseString(HttpRequestBase httpRequest) throws Exception { CloseableHttpResponse response = httpClient.execute(httpRequest); HttpEntity entity = response.getEntity(); - return EntityUtils.toString(entity, DEFAULT_CHARSET); + return EntityUtils.toString(entity, StandardCharsets.UTF_8); } private void createHttpRequest(String msg) throws MalformedURLException, URISyntaxException { @@ -157,7 +157,8 @@ private void setMsgInUrl(String msg) { type = URL_SPLICE_CHAR; } try { - url = String.format("%s%s%s=%s", url, type, contentField, URLEncoder.encode(msg, DEFAULT_CHARSET)); + url = String.format("%s%s%s=%s", url, type, contentField, + URLEncoder.encode(msg, StandardCharsets.UTF_8.name())); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } @@ -190,7 +191,7 @@ private void setMsgInRequestBody(String msg) { } // set msg content field objectNode.put(contentField, msg); - StringEntity entity = new StringEntity(JSONUtils.toJsonString(objectNode), DEFAULT_CHARSET); + StringEntity entity = new StringEntity(JSONUtils.toJsonString(objectNode), StandardCharsets.UTF_8); ((HttpPost) httpRequest).setEntity(entity); } catch (Exception e) { log.error("send http alert msg exception : {}", e.getMessage()); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSender.java index d27745049e07..1106e6799f11 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSender.java @@ -34,6 +34,7 @@ import org.apache.http.util.EntityUtils; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -90,7 +91,7 @@ private String sendMsg(AlertData alertData) throws IOException { } HttpEntity entity = response.getEntity(); - resp = EntityUtils.toString(entity, "utf-8"); + resp = EntityUtils.toString(entity, StandardCharsets.UTF_8); EntityUtils.consume(entity); log.error( "Prometheus alert manager send alert failed, http status code: {}, title: {} ,content: {}, resp: {}", diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/java/org/apache/dolphinscheduler/plugin/alert/script/ProcessUtilsTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/java/org/apache/dolphinscheduler/plugin/alert/script/ProcessUtilsTest.java index a34f062264bc..3d85d5d638a3 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/java/org/apache/dolphinscheduler/plugin/alert/script/ProcessUtilsTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/java/org/apache/dolphinscheduler/plugin/alert/script/ProcessUtilsTest.java @@ -33,6 +33,7 @@ public class ProcessUtilsTest { @Test public void testExecuteScript() { - ProcessUtils.executeScript(cmd); + int code = ProcessUtils.executeScript(cmd); + assert code != -1; } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSender.java index 60b2f8281a63..4096ccc998c7 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSender.java @@ -29,6 +29,7 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; @@ -81,11 +82,11 @@ public String sendMessage(String title, String content) { } HttpPost httpPost = new HttpPost(webHookUrl); - httpPost.setEntity(new StringEntity(JSONUtils.toJsonString(paramMap), "UTF-8")); + httpPost.setEntity(new StringEntity(JSONUtils.toJsonString(paramMap), StandardCharsets.UTF_8)); CloseableHttpResponse response = httpClient.execute(httpPost); HttpEntity entity = response.getEntity(); - return EntityUtils.toString(entity, "UTF-8"); + return EntityUtils.toString(entity, StandardCharsets.UTF_8); } catch (Exception e) { log.error("Send message to slack error.", e); return "System Exception"; diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSender.java index 8aba9f5c2b0b..129bc62c1c2d 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSender.java @@ -153,7 +153,7 @@ private String sendInvoke(String title, String content) throws IOException { String resp; try { HttpEntity entity = response.getEntity(); - resp = EntityUtils.toString(entity, "UTF-8"); + resp = EntityUtils.toString(entity, StandardCharsets.UTF_8); EntityUtils.consume(entity); } finally { response.close(); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatAlertConstants.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatAlertConstants.java index 7f5eaef4f968..76ad4800153a 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatAlertConstants.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatAlertConstants.java @@ -23,8 +23,6 @@ public final class WeChatAlertConstants { static final String MARKDOWN_ENTER = "\n"; - static final String CHARSET = "UTF-8"; - static final String WE_CHAT_PUSH_URL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}"; static final String WE_CHAT_APP_CHAT_PUSH_URL = "https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token" + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSender.java index 4b49e0436dc6..c5ffec1f468c 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSender.java @@ -38,6 +38,7 @@ import org.apache.http.util.EntityUtils; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -45,6 +46,8 @@ import java.util.Map.Entry; import java.util.Set; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -85,12 +88,12 @@ private static String post(String url, String data) throws IOException { CloseableHttpClient httpClient = HttpClients.custom().setRetryHandler(HttpServiceRetryStrategy.retryStrategy).build()) { HttpPost httpPost = new HttpPost(url); - httpPost.setEntity(new StringEntity(data, WeChatAlertConstants.CHARSET)); + httpPost.setEntity(new StringEntity(data, StandardCharsets.UTF_8)); CloseableHttpResponse response = httpClient.execute(httpPost); String resp; try { HttpEntity entity = response.getEntity(); - resp = EntityUtils.toString(entity, WeChatAlertConstants.CHARSET); + resp = EntityUtils.toString(entity, StandardCharsets.UTF_8); EntityUtils.consume(entity); } finally { response.close(); @@ -142,7 +145,7 @@ private static String get(String url) throws IOException { HttpGet httpGet = new HttpGet(url); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { HttpEntity entity = response.getEntity(); - resp = EntityUtils.toString(entity, WeChatAlertConstants.CHARSET); + resp = EntityUtils.toString(entity, StandardCharsets.UTF_8); EntityUtils.consume(entity); } @@ -259,6 +262,8 @@ private String getToken() { return null; } + @Getter + @Setter static final class WeChatSendMsgResponse { private Integer errcode; @@ -267,22 +272,6 @@ static final class WeChatSendMsgResponse { public WeChatSendMsgResponse() { } - public Integer getErrcode() { - return this.errcode; - } - - public void setErrcode(Integer errcode) { - this.errcode = errcode; - } - - public String getErrmsg() { - return this.errmsg; - } - - public void setErrmsg(String errmsg) { - this.errmsg = errmsg; - } - public boolean equals(final Object o) { if (o == this) { return true; diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java index b5bbf740e9a2..ebf668a3124b 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java @@ -242,11 +242,6 @@ private Constants() { */ public static final String HTTP_X_REAL_IP = "X-Real-IP"; - /** - * UTF-8 - */ - public static final String UTF_8 = "UTF-8"; - /** * user name regex */ diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/FileUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/FileUtils.java index fee1d9a95c50..60629576d9c4 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/FileUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/FileUtils.java @@ -22,7 +22,6 @@ import static org.apache.dolphinscheduler.common.constants.Constants.FORMAT_S_S; import static org.apache.dolphinscheduler.common.constants.Constants.RESOURCE_VIEW_SUFFIXES; import static org.apache.dolphinscheduler.common.constants.Constants.RESOURCE_VIEW_SUFFIXES_DEFAULT_VALUE; -import static org.apache.dolphinscheduler.common.constants.Constants.UTF_8; import static org.apache.dolphinscheduler.common.constants.DateConstants.YYYYMMDDHHMMSS; import org.apache.commons.io.IOUtils; @@ -207,7 +206,7 @@ public static String readFile2Str(InputStream inputStream) { while ((length = inputStream.read(buffer)) != -1) { output.write(buffer, 0, length); } - return output.toString(UTF_8); + return output.toString(StandardCharsets.UTF_8.name()); } catch (Exception e) { log.error(e.getMessage(), e); throw new RuntimeException(e); diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/HttpUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/HttpUtils.java index 5c79fff951b8..e5d22561508e 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/HttpUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/HttpUtils.java @@ -36,6 +36,7 @@ import org.apache.http.util.EntityUtils; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -143,7 +144,7 @@ public static String getResponseContentString(HttpGet httpGet, CloseableHttpClie } HttpEntity entity = response.getEntity(); - return entity != null ? EntityUtils.toString(entity, Constants.UTF_8) : null; + return entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : null; } catch (IOException e) { log.error("Error executing HTTP GET request", e); return null; From 1d13ef09c3607870f7309d9f41dc5deac8d209ee Mon Sep 17 00:00:00 2001 From: Evan Sun Date: Wed, 24 Apr 2024 23:31:56 +0800 Subject: [PATCH 049/165] [TEST] increase coverage of project parameter service test (#15905) Co-authored-by: abzymeinsjtu --- .../impl/ProjectParameterServiceImpl.java | 6 +- .../service/ProjectParameterServiceTest.java | 198 ++++++++++++++---- 2 files changed, 154 insertions(+), 50 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java index e0011096e464..17d1d04712c4 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectParameterServiceImpl.java @@ -227,11 +227,7 @@ public Result batchDeleteProjectParametersByCodes(User loginUser, long projectCo } for (ProjectParameter projectParameter : projectParameterList) { - try { - this.deleteProjectParametersByCode(loginUser, projectCode, projectParameter.getCode()); - } catch (Exception e) { - throw new ServiceException(Status.DELETE_PROJECT_PARAMETER_ERROR, e.getMessage()); - } + this.deleteProjectParametersByCode(loginUser, projectCode, projectParameter.getCode()); } putMsg(result, Status.SUCCESS); diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectParameterServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectParameterServiceTest.java index 7a3fb1b68d32..ca51690ce6b4 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectParameterServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectParameterServiceTest.java @@ -17,27 +17,40 @@ package org.apache.dolphinscheduler.api.service; +import static org.apache.dolphinscheduler.api.utils.ServiceTestUtil.getGeneralUser; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +import org.apache.dolphinscheduler.api.AssertionsHelper; import org.apache.dolphinscheduler.api.enums.Status; import org.apache.dolphinscheduler.api.service.impl.ProjectParameterServiceImpl; import org.apache.dolphinscheduler.api.service.impl.ProjectServiceImpl; import org.apache.dolphinscheduler.api.utils.Result; -import org.apache.dolphinscheduler.common.enums.UserType; +import org.apache.dolphinscheduler.common.utils.CodeGenerateUtils; import org.apache.dolphinscheduler.dao.entity.Project; import org.apache.dolphinscheduler.dao.entity.ProjectParameter; import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.dao.mapper.ProjectMapper; import org.apache.dolphinscheduler.dao.mapper.ProjectParameterMapper; -import org.junit.jupiter.api.Assertions; +import java.util.Collections; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public class ProjectParameterServiceTest { @@ -60,92 +73,187 @@ public class ProjectParameterServiceTest { public void testCreateProjectParameter() { User loginUser = getGeneralUser(); - // PROJECT_PARAMETER_ALREADY_EXISTS - Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); - Mockito.when(projectParameterMapper.selectOne(Mockito.any())).thenReturn(getProjectParameter()); - Mockito.when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) - .thenReturn(true); + // PERMISSION DENIED + when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + .thenReturn(false); Result result = projectParameterService.createProjectParameter(loginUser, projectCode, "key", "value"); - Assertions.assertEquals(Status.PROJECT_PARAMETER_ALREADY_EXISTS.getCode(), result.getCode()); + assertNull(result.getData()); + assertNull(result.getCode()); + assertNull(result.getMsg()); + + when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + .thenReturn(true); + + // CODE GENERATION ERROR + try (MockedStatic ignored = Mockito.mockStatic(CodeGenerateUtils.class)) { + when(CodeGenerateUtils.genCode()).thenThrow(CodeGenerateUtils.CodeGenerateException.class); + + result = projectParameterService.createProjectParameter(loginUser, projectCode, "key", "value"); + assertEquals(Status.CREATE_PROJECT_PARAMETER_ERROR.getCode(), result.getCode()); + } + + // PROJECT_PARAMETER_ALREADY_EXISTS + when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); + when(projectParameterMapper.selectOne(Mockito.any())).thenReturn(getProjectParameter()); + result = projectParameterService.createProjectParameter(loginUser, projectCode, "key", "value"); + assertEquals(Status.PROJECT_PARAMETER_ALREADY_EXISTS.getCode(), result.getCode()); + + // INSERT DATA ERROR + when(projectParameterMapper.selectOne(Mockito.any())).thenReturn(null); + when(projectParameterMapper.insert(Mockito.any())).thenReturn(-1); + result = projectParameterService.createProjectParameter(loginUser, projectCode, "key1", "value"); + assertEquals(Status.CREATE_PROJECT_PARAMETER_ERROR.getCode(), result.getCode()); // SUCCESS - Mockito.when(projectParameterMapper.selectOne(Mockito.any())).thenReturn(null); - Mockito.when(projectParameterMapper.insert(Mockito.any())).thenReturn(1); + when(projectParameterMapper.insert(Mockito.any())).thenReturn(1); result = projectParameterService.createProjectParameter(loginUser, projectCode, "key1", "value"); - Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + assertEquals(Status.SUCCESS.getCode(), result.getCode()); } @Test public void testUpdateProjectParameter() { User loginUser = getGeneralUser(); + // NO PERMISSION + when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + .thenReturn(false); + Result result = projectParameterService.updateProjectParameter(loginUser, projectCode, 1, "key", "value"); + assertNull(result.getData()); + assertNull(result.getCode()); + assertNull(result.getMsg()); + // PROJECT_PARAMETER_NOT_EXISTS - Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); - Mockito.when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); + when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) .thenReturn(true); - Mockito.when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(null); - Result result = projectParameterService.updateProjectParameter(loginUser, projectCode, 1, "key", "value"); - Assertions.assertEquals(Status.PROJECT_PARAMETER_NOT_EXISTS.getCode(), result.getCode()); + when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(null); + result = projectParameterService.updateProjectParameter(loginUser, projectCode, 1, "key", "value"); + assertEquals(Status.PROJECT_PARAMETER_NOT_EXISTS.getCode(), result.getCode()); // PROJECT_PARAMETER_ALREADY_EXISTS - Mockito.when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(getProjectParameter()); - Mockito.when(projectParameterMapper.selectOne(Mockito.any())).thenReturn(getProjectParameter()); + when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(getProjectParameter()); + when(projectParameterMapper.selectOne(Mockito.any())).thenReturn(getProjectParameter()); result = projectParameterService.updateProjectParameter(loginUser, projectCode, 1, "key", "value"); - Assertions.assertEquals(Status.PROJECT_PARAMETER_ALREADY_EXISTS.getCode(), result.getCode()); + assertEquals(Status.PROJECT_PARAMETER_ALREADY_EXISTS.getCode(), result.getCode()); + + // PROJECT_UPDATE_ERROR + when(projectParameterMapper.selectOne(Mockito.any())).thenReturn(null); + when(projectParameterMapper.updateById(Mockito.any())).thenReturn(-1); + result = projectParameterService.updateProjectParameter(loginUser, projectCode, 1, "key1", "value"); + assertEquals(Status.UPDATE_PROJECT_PARAMETER_ERROR.getCode(), result.getCode()); // SUCCESS - Mockito.when(projectParameterMapper.selectOne(Mockito.any())).thenReturn(null); - Mockito.when(projectParameterMapper.updateById(Mockito.any())).thenReturn(1); + when(projectParameterMapper.updateById(Mockito.any())).thenReturn(1); result = projectParameterService.updateProjectParameter(loginUser, projectCode, 1, "key1", "value"); - Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + assertEquals(Status.SUCCESS.getCode(), result.getCode()); ProjectParameter projectParameter = (ProjectParameter) result.getData(); - Assertions.assertNotNull(projectParameter.getOperator()); - Assertions.assertNotNull(projectParameter.getUpdateTime()); + assertNotNull(projectParameter.getOperator()); + assertNotNull(projectParameter.getUpdateTime()); } @Test public void testDeleteProjectParametersByCode() { User loginUser = getGeneralUser(); + // NO PERMISSION + when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + .thenReturn(false); + Result result = projectParameterService.deleteProjectParametersByCode(loginUser, projectCode, 1); + assertNull(result.getData()); + assertNull(result.getCode()); + assertNull(result.getMsg()); + // PROJECT_PARAMETER_NOT_EXISTS - Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); - Mockito.when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); + when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) .thenReturn(true); - Mockito.when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(null); - Result result = projectParameterService.deleteProjectParametersByCode(loginUser, projectCode, 1); - Assertions.assertEquals(Status.PROJECT_PARAMETER_NOT_EXISTS.getCode(), result.getCode()); + when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(null); + result = projectParameterService.deleteProjectParametersByCode(loginUser, projectCode, 1); + assertEquals(Status.PROJECT_PARAMETER_NOT_EXISTS.getCode(), result.getCode()); + + // DATABASE OPERATION ERROR + when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(getProjectParameter()); + when(projectParameterMapper.deleteById(Mockito.anyInt())).thenReturn(-1); + result = projectParameterService.deleteProjectParametersByCode(loginUser, projectCode, 1); + assertEquals(Status.DELETE_PROJECT_PARAMETER_ERROR.getCode(), result.getCode()); // SUCCESS - Mockito.when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(getProjectParameter()); - Mockito.when(projectParameterMapper.deleteById(Mockito.anyInt())).thenReturn(1); + when(projectParameterMapper.deleteById(Mockito.anyInt())).thenReturn(1); result = projectParameterService.deleteProjectParametersByCode(loginUser, projectCode, 1); - Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + assertEquals(Status.SUCCESS.getCode(), result.getCode()); } @Test public void testQueryProjectParameterByCode() { User loginUser = getGeneralUser(); + // NO PERMISSION + when(projectService.hasProjectAndPerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class), + Mockito.any())) + .thenReturn(false); + + Result result = projectParameterService.queryProjectParameterByCode(loginUser, projectCode, 1); + assertNull(result.getData()); + assertNull(result.getCode()); + assertNull(result.getMsg()); + // PROJECT_PARAMETER_NOT_EXISTS - Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); - Mockito.when(projectService.hasProjectAndPerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class), + when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); + when(projectService.hasProjectAndPerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class), Mockito.any())).thenReturn(true); - Mockito.when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(null); - Result result = projectParameterService.queryProjectParameterByCode(loginUser, projectCode, 1); - Assertions.assertEquals(Status.PROJECT_PARAMETER_NOT_EXISTS.getCode(), result.getCode()); + when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(null); + result = projectParameterService.queryProjectParameterByCode(loginUser, projectCode, 1); + assertEquals(Status.PROJECT_PARAMETER_NOT_EXISTS.getCode(), result.getCode()); // SUCCESS - Mockito.when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(getProjectParameter()); + when(projectParameterMapper.queryByCode(Mockito.anyLong())).thenReturn(getProjectParameter()); result = projectParameterService.queryProjectParameterByCode(loginUser, projectCode, 1); - Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + assertEquals(Status.SUCCESS.getCode(), result.getCode()); } - private User getGeneralUser() { - User loginUser = new User(); - loginUser.setUserType(UserType.GENERAL_USER); - loginUser.setUserName("userName"); - loginUser.setId(1); - return loginUser; + @Test + public void testQueryProjectParameterListPaging() { + User loginUser = getGeneralUser(); + Integer pageSize = 10; + Integer pageNo = 1; + + // NO PERMISSION + when(projectService.hasProjectAndPerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class), + Mockito.any())) + .thenReturn(false); + + Result result = + projectParameterService.queryProjectParameterListPaging(loginUser, projectCode, pageSize, pageNo, null); + assertNull(result.getData()); + assertNull(result.getCode()); + assertNull(result.getMsg()); + + // SUCCESS + when(projectService.hasProjectAndPerm(any(), any(), any(Result.class), any())) + .thenReturn(true); + + Page page = new Page<>(pageNo, pageSize); + page.setRecords(Collections.singletonList(getProjectParameter())); + + when(projectParameterMapper.queryProjectParameterListPaging(any(), anyLong(), any(), any())).thenReturn(page); + result = projectParameterService.queryProjectParameterListPaging(loginUser, projectCode, pageSize, pageNo, + null); + assertEquals(Status.SUCCESS.getCode(), result.getCode()); + } + + @Test + public void testBatchDeleteProjectParametersByCodes() { + User loginUser = getGeneralUser(); + + Result result = projectParameterService.batchDeleteProjectParametersByCodes(loginUser, projectCode, ""); + assertEquals(Status.PROJECT_PARAMETER_CODE_EMPTY.getCode(), result.getCode()); + + when(projectParameterMapper.queryByCodes(any())).thenReturn(Collections.singletonList(getProjectParameter())); + + AssertionsHelper.assertThrowsServiceException(Status.PROJECT_PARAMETER_NOT_EXISTS, + () -> projectParameterService.batchDeleteProjectParametersByCodes(loginUser, projectCode, "1,2")); + + projectParameterService.batchDeleteProjectParametersByCodes(loginUser, projectCode, "1"); } private Project getProject(long projectCode) { From b3b8c0784dfc31b516b13601a311bc78550bf931 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 25 Apr 2024 11:05:51 +0800 Subject: [PATCH 050/165] Fix kill dynamic task doesn't kill the wait to run workflow instances (#15896) --- .../dao/mapper/CommandMapper.java | 9 +++- .../dao/mapper/CommandMapper.xml | 7 +++ .../dao/mapper/CommandMapperTest.java | 11 +++- .../runner/task/dynamic/DynamicLogicTask.java | 51 ++++++++++++++++++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/CommandMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/CommandMapper.java index a8490cbef7c2..9fb664322764 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/CommandMapper.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/CommandMapper.java @@ -34,8 +34,9 @@ public interface CommandMapper extends BaseMapper { /** * count command state - * @param startTime startTime - * @param endTime endTime + * + * @param startTime startTime + * @param endTime endTime * @param projectCodes projectCodes * @return CommandCount list */ @@ -46,15 +47,19 @@ List countCommandState( /** * query command page + * * @return */ List queryCommandPage(@Param("limit") int limit, @Param("offset") int offset); /** * query command page by slot + * * @return command list */ List queryCommandPageBySlot(@Param("limit") int limit, @Param("masterCount") int masterCount, @Param("thisMasterSlot") int thisMasterSlot); + + void deleteByWorkflowInstanceIds(@Param("workflowInstanceIds") List workflowInstanceIds); } diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/CommandMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/CommandMapper.xml index c950f664138a..56db890ef07a 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/CommandMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/CommandMapper.xml @@ -47,4 +47,11 @@ order by process_instance_priority, id asc limit #{limit} + + delete from t_ds_command + where process_instance_id in + + #{i} + + diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/CommandMapperTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/CommandMapperTest.java index 3d45477d858b..2d367e46e4c4 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/CommandMapperTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/CommandMapperTest.java @@ -17,6 +17,8 @@ package org.apache.dolphinscheduler.dao.mapper; +import static com.google.common.truth.Truth.assertThat; + import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.enums.CommandType; import org.apache.dolphinscheduler.common.enums.FailureStrategy; @@ -173,6 +175,14 @@ public void testQueryCommandPageBySlot() { toTestQueryCommandPageBySlot(masterCount, thisMasterSlot); } + @Test + void deleteByWorkflowInstanceIds() { + Command command = createCommand(); + assertThat(commandMapper.selectList(null)).isNotEmpty(); + commandMapper.deleteByWorkflowInstanceIds(Lists.newArrayList(command.getProcessInstanceId())); + assertThat(commandMapper.selectList(null)).isEmpty(); + } + private boolean toTestQueryCommandPageBySlot(int masterCount, int thisMasterSlot) { Command command = createCommand(); Integer id = command.getId(); @@ -280,5 +290,4 @@ private Command createCommand(CommandType commandType, long processDefinitionCod return command; } - } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicLogicTask.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicLogicTask.java index 3baa10b343ef..12cae5c53ea0 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicLogicTask.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicLogicTask.java @@ -252,12 +252,61 @@ private List getDynamicInputParameters() { @Override public void kill() { try { - changeRunningSubprocessInstancesToStop(WorkflowExecutionStatus.READY_STOP); + doKillSubWorkflowInstances(); } catch (MasterTaskExecuteException e) { log.error("kill {} error", taskInstance.getName(), e); } } + private void doKillSubWorkflowInstances() throws MasterTaskExecuteException { + List existsSubProcessInstanceList = + subWorkflowService.getAllDynamicSubWorkflow(processInstance.getId(), taskInstance.getTaskCode()); + if (CollectionUtils.isEmpty(existsSubProcessInstanceList)) { + return; + } + + commandMapper.deleteByWorkflowInstanceIds( + existsSubProcessInstanceList.stream().map(ProcessInstance::getId).collect(Collectors.toList())); + + List runningSubProcessInstanceList = + subWorkflowService.filterRunningProcessInstances(existsSubProcessInstanceList); + doKillRunningSubWorkflowInstances(runningSubProcessInstanceList); + + List waitToRunProcessInstances = + subWorkflowService.filterWaitToRunProcessInstances(existsSubProcessInstanceList); + doKillWaitToRunSubWorkflowInstances(waitToRunProcessInstances); + + this.haveBeenCanceled = true; + } + + private void doKillRunningSubWorkflowInstances(List runningSubProcessInstanceList) throws MasterTaskExecuteException { + for (ProcessInstance subProcessInstance : runningSubProcessInstanceList) { + subProcessInstance.setState(WorkflowExecutionStatus.READY_STOP); + processInstanceDao.updateById(subProcessInstance); + if (subProcessInstance.getState().isFinished()) { + log.info("The process instance [{}] is finished, no need to stop", subProcessInstance.getId()); + continue; + } + try { + sendToSubProcess(taskExecutionContext, subProcessInstance); + log.info("Success send [{}] request to SubWorkflow's master: {}", WorkflowExecutionStatus.READY_STOP, + subProcessInstance.getHost()); + } catch (Exception e) { + throw new MasterTaskExecuteException( + String.format("Send stop request to SubWorkflow's master: %s failed", + subProcessInstance.getHost()), + e); + } + } + } + + private void doKillWaitToRunSubWorkflowInstances(List waitToRunWorkflowInstances) { + for (ProcessInstance subProcessInstance : waitToRunWorkflowInstances) { + subProcessInstance.setState(WorkflowExecutionStatus.STOP); + processInstanceDao.updateById(subProcessInstance); + } + } + private void changeRunningSubprocessInstancesToStop(WorkflowExecutionStatus stopStatus) throws MasterTaskExecuteException { this.haveBeenCanceled = true; List existsSubProcessInstanceList = From 446f6ba72b8347ad817d8f62cdd8d258ace7adf4 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 25 Apr 2024 18:14:48 +0800 Subject: [PATCH 051/165] Fix auto create tennat concurrently will cause the task failed (#15909) --- .../common/utils/OSUtils.java | 25 ++++++++----------- .../utils/TaskExecutionContextUtils.java | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OSUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OSUtils.java index beca53c3fd59..dbfcea2ed8b7 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OSUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OSUtils.java @@ -183,11 +183,10 @@ public static boolean existTenantCodeInLinux(String tenantCode) { * * @param userName user name */ - public static void createUserIfAbsent(String userName) { + public static synchronized void createUserIfAbsent(String userName) { // if not exists this user, then create if (!getUserList().contains(userName)) { - boolean isSuccess = createUser(userName); - log.info("create user {} {}", userName, isSuccess ? "success" : "fail"); + createUser(userName); } } @@ -197,13 +196,12 @@ public static void createUserIfAbsent(String userName) { * @param userName user name * @return true if creation was successful, otherwise false */ - public static boolean createUser(String userName) { + public static void createUser(String userName) { try { String userGroup = getGroup(); if (StringUtils.isEmpty(userGroup)) { - String errorLog = String.format("%s group does not exist for this operating system.", userGroup); - log.error(errorLog); - return false; + throw new UnsupportedOperationException( + "There is no userGroup exist cannot create tenant, please create userGroupFirst"); } if (SystemUtils.IS_OS_MAC) { createMacUser(userName, userGroup); @@ -212,18 +210,17 @@ public static boolean createUser(String userName) { } else { createLinuxUser(userName, userGroup); } - return true; + log.info("Create tenant {} under userGroup: {} success", userName, userGroup); } catch (Exception e) { - log.error(e.getMessage(), e); + throw new RuntimeException("Create tenant: {} failed", e); } - return false; } /** * create linux user * - * @param userName user name + * @param userName user name * @param userGroup user group * @throws IOException in case of an I/O error */ @@ -237,7 +234,7 @@ private static void createLinuxUser(String userName, String userGroup) throws IO /** * create mac user (Supports Mac OSX 10.10+) * - * @param userName user name + * @param userName user name * @param userGroup user group * @throws IOException in case of an I/O error */ @@ -256,7 +253,7 @@ private static void createMacUser(String userName, String userGroup) throws IOEx /** * create windows user * - * @param userName user name + * @param userName user name * @param userGroup user group * @throws IOException in case of an I/O error */ @@ -304,7 +301,7 @@ public static String getGroup() throws IOException { * get sudo command * * @param tenantCode tenantCode - * @param command command + * @param command command * @return result of sudo execute command */ public static String getSudoCmd(String tenantCode, String command) { diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/utils/TaskExecutionContextUtils.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/utils/TaskExecutionContextUtils.java index 3cda6ac099e0..f43b19c4444c 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/utils/TaskExecutionContextUtils.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/utils/TaskExecutionContextUtils.java @@ -78,7 +78,7 @@ public static String getOrCreateTenant(WorkerConfig workerConfig, TaskExecutionC throw ex; } catch (Exception ex) { throw new TaskException( - String.format("TenantCode: %s doesn't exist", taskExecutionContext.getTenantCode())); + String.format("TenantCode: %s doesn't exist", taskExecutionContext.getTenantCode()), ex); } } From d1135eabc7df9358fd210e423785d1067af64698 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 25 Apr 2024 19:39:56 +0800 Subject: [PATCH 052/165] [DSIP-34] Change required_approving_review_count to 2 (#15914) --- .asf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.asf.yaml b/.asf.yaml index cf409dc6a353..84619447beee 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -49,4 +49,4 @@ github: - "Mergeable: milestone-label-check" required_pull_request_reviews: dismiss_stale_reviews: true - required_approving_review_count: 1 + required_approving_review_count: 2 From b29965bdce2b5b83c1ffe237265e6f53f01e11cf Mon Sep 17 00:00:00 2001 From: DaqianLiao <360989637@qq.com> Date: Fri, 26 Apr 2024 11:24:05 +0800 Subject: [PATCH 053/165] [Fix] In updateWorkerNodes method, the workerNodeInfoWriteLock should be used. #15898 (#15903) Co-authored-by: answerliao --- .../server/master/registry/ServerNodeManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/ServerNodeManager.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/ServerNodeManager.java index 258acd8f6e92..f066f0403d8d 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/ServerNodeManager.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/registry/ServerNodeManager.java @@ -245,7 +245,7 @@ private void updateMasterNodes() { } private void updateWorkerNodes() { - workerGroupWriteLock.lock(); + workerNodeInfoWriteLock.lock(); try { Map workerNodeMaps = registryClient.getServerMaps(RegistryNodeType.WORKER); for (Map.Entry entry : workerNodeMaps.entrySet()) { @@ -254,7 +254,7 @@ private void updateWorkerNodes() { workerNodeInfo.put(nodeAddress, workerHeartBeat); } } finally { - workerGroupWriteLock.unlock(); + workerNodeInfoWriteLock.unlock(); } } From 7a55adeae9d9c5e8bc813d952322c8da1aabdc99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=8F=AF=E8=80=90?= <46134044+sdhzwc@users.noreply.github.com> Date: Sun, 28 Apr 2024 11:02:44 +0800 Subject: [PATCH 054/165] [Improvement-15919][datasource] Improvement datasource get name (#15920) --- .../AbstractDataSourceProcessor.java | 2 +- .../api/plugin/DataSourceClientProvider.java | 8 +-- .../AthenaDataSourceChannelFactory.java | 3 +- .../AzureSQLDataSourceChannelFactory.java | 3 +- .../ClickHouseDataSourceChannelFactory.java | 3 +- .../DamengDataSourceChannelFactory.java | 2 +- .../DatabendDataSourceChannelFactory.java | 3 +- .../DatabendDataSourceProcessorTest.java | 2 +- .../db2/DB2DataSourceChannelFactory.java | 3 +- .../doris/DorisDataSourceChannelFactory.java | 2 +- .../hana/HanaDataSourceChannelFactory.java | 3 +- .../hive/HiveDataSourceChannelFactory.java | 3 +- .../k8s/K8sDataSourceChannelFactory.java | 3 +- .../k8s/param/K8sDataSourceProcessor.java | 2 +- .../KyuubiDataSourceChannelFactory.java | 3 +- .../param/KyuubiDataSourceProcessorTest.java | 2 +- .../mysql/MySQLDataSourceChannelFactory.java | 3 +- .../OceanBaseDataSourceChannelFactory.java | 3 +- .../OracleDataSourceChannelFactory.java | 3 +- .../PostgreSQLDataSourceChannelFactory.java | 3 +- .../PrestoDataSourceChannelFactory.java | 3 +- .../RedshiftDataSourceChannelFactory.java | 3 +- .../SagemakerDataSourceChannelFactory.java | 3 +- .../param/SagemakerDataSourceProcessor.java | 2 +- .../SnowflakeDataSourceChannelFactory.java | 3 +- .../SnowflakeDataSourceProcessorTest.java | 2 +- .../spark/SparkDataSourceChannelFactory.java | 3 +- .../SQLServerDataSourceChannelFactory.java | 3 +- .../ssh/SSHDataSourceChannelFactory.java | 3 +- .../ssh/param/SSHDataSourceProcessor.java | 2 +- .../StarRocksDataSourceChannelFactory.java | 2 +- .../trino/TrinoDataSourceChannelFactory.java | 3 +- .../VerticaDataSourceChannelFactory.java | 3 +- .../ZeppelinDataSourceChannelFactory.java | 3 +- .../param/ZeppelinDataSourceProcessor.java | 2 +- .../dolphinscheduler/spi/enums/DbType.java | 62 ++++++++++--------- 36 files changed, 95 insertions(+), 66 deletions(-) diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/AbstractDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/AbstractDataSourceProcessor.java index 98222a2c5add..4acf531ddc66 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/AbstractDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/AbstractDataSourceProcessor.java @@ -118,7 +118,7 @@ protected Map transformOtherParamToMap(String other) { @Override public String getDatasourceUniqueId(ConnectionParam connectionParam, DbType dbType) { BaseConnectionParam baseConnectionParam = (BaseConnectionParam) connectionParam; - return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getDescp(), baseConnectionParam.getUser(), + return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getName(), baseConnectionParam.getUser(), PasswordUtils.encodePassword(baseConnectionParam.getPassword()), baseConnectionParam.getJdbcUrl()); } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/plugin/DataSourceClientProvider.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/plugin/DataSourceClientProvider.java index 839a4c5d61db..7223fe62a387 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/plugin/DataSourceClientProvider.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/plugin/DataSourceClientProvider.java @@ -69,9 +69,9 @@ public static DataSourceClient getPooledDataSourceClient(DbType dbType, String datasourceUniqueId = DataSourceUtils.getDatasourceUniqueId(baseConnectionParam, dbType); return POOLED_DATASOURCE_CLIENT_CACHE.get(datasourceUniqueId, () -> { Map dataSourceChannelMap = dataSourcePluginManager.getDataSourceChannelMap(); - DataSourceChannel dataSourceChannel = dataSourceChannelMap.get(dbType.getDescp()); + DataSourceChannel dataSourceChannel = dataSourceChannelMap.get(dbType.getName()); if (null == dataSourceChannel) { - throw new RuntimeException(String.format("datasource plugin '%s' is not found", dbType.getDescp())); + throw new RuntimeException(String.format("datasource plugin '%s' is not found", dbType.getName())); } return dataSourceChannel.createPooledDataSourceClient(baseConnectionParam, dbType); }); @@ -85,9 +85,9 @@ public static Connection getPooledConnection(DbType dbType, public static AdHocDataSourceClient getAdHocDataSourceClient(DbType dbType, ConnectionParam connectionParam) { BaseConnectionParam baseConnectionParam = (BaseConnectionParam) connectionParam; Map dataSourceChannelMap = dataSourcePluginManager.getDataSourceChannelMap(); - DataSourceChannel dataSourceChannel = dataSourceChannelMap.get(dbType.getDescp()); + DataSourceChannel dataSourceChannel = dataSourceChannelMap.get(dbType.getName()); if (null == dataSourceChannel) { - throw new RuntimeException(String.format("datasource plugin '%s' is not found", dbType.getDescp())); + throw new RuntimeException(String.format("datasource plugin '%s' is not found", dbType.getName())); } return dataSourceChannel.createAdHocDataSourceClient(baseConnectionParam, dbType); } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-athena/src/main/java/org/apache/dolphinscheduler/plugin/datasource/athena/AthenaDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-athena/src/main/java/org/apache/dolphinscheduler/plugin/datasource/athena/AthenaDataSourceChannelFactory.java index 1b2ed367d0a5..b4759db39ab2 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-athena/src/main/java/org/apache/dolphinscheduler/plugin/datasource/athena/AthenaDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-athena/src/main/java/org/apache/dolphinscheduler/plugin/datasource/athena/AthenaDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -32,6 +33,6 @@ public DataSourceChannel create() { @Override public String getName() { - return "athena"; + return DbType.ATHENA.getName(); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-azure-sql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/azuresql/AzureSQLDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-azure-sql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/azuresql/AzureSQLDataSourceChannelFactory.java index 5966848f3303..2b8cdca973a4 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-azure-sql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/azuresql/AzureSQLDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-azure-sql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/azuresql/AzureSQLDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class AzureSQLDataSourceChannelFactory implements DataSourceChannelFactor @Override public String getName() { - return "azuresql"; + return DbType.AZURESQL.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-clickhouse/src/main/java/org/apache/dolphinscheduler/plugin/datasource/clickhouse/ClickHouseDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-clickhouse/src/main/java/org/apache/dolphinscheduler/plugin/datasource/clickhouse/ClickHouseDataSourceChannelFactory.java index d756226522fb..77d0feb1d18e 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-clickhouse/src/main/java/org/apache/dolphinscheduler/plugin/datasource/clickhouse/ClickHouseDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-clickhouse/src/main/java/org/apache/dolphinscheduler/plugin/datasource/clickhouse/ClickHouseDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class ClickHouseDataSourceChannelFactory implements DataSourceChannelFact @Override public String getName() { - return "clickhouse"; + return DbType.CLICKHOUSE.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-dameng/src/main/java/org/apache/dolphinscheduler/plugin/datasource/dameng/DamengDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-dameng/src/main/java/org/apache/dolphinscheduler/plugin/datasource/dameng/DamengDataSourceChannelFactory.java index 945f6610c0e7..84ae08013460 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-dameng/src/main/java/org/apache/dolphinscheduler/plugin/datasource/dameng/DamengDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-dameng/src/main/java/org/apache/dolphinscheduler/plugin/datasource/dameng/DamengDataSourceChannelFactory.java @@ -28,7 +28,7 @@ public class DamengDataSourceChannelFactory implements DataSourceChannelFactory @Override public String getName() { - return DbType.DAMENG.getDescp(); + return DbType.DAMENG.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-databend/src/main/java/org/apache/dolphinscheduler/plugin/datasource/databend/DatabendDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-databend/src/main/java/org/apache/dolphinscheduler/plugin/datasource/databend/DatabendDataSourceChannelFactory.java index 0ea40c3b132e..3c86601dd717 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-databend/src/main/java/org/apache/dolphinscheduler/plugin/datasource/databend/DatabendDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-databend/src/main/java/org/apache/dolphinscheduler/plugin/datasource/databend/DatabendDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class DatabendDataSourceChannelFactory implements DataSourceChannelFactor @Override public String getName() { - return "databend"; + return DbType.DATABEND.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-databend/src/test/java/org/apache/dolphinscheduler/plugin/datasource/databend/param/DatabendDataSourceProcessorTest.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-databend/src/test/java/org/apache/dolphinscheduler/plugin/datasource/databend/param/DatabendDataSourceProcessorTest.java index f225a2fd3d2b..cb41c6562b20 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-databend/src/test/java/org/apache/dolphinscheduler/plugin/datasource/databend/param/DatabendDataSourceProcessorTest.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-databend/src/test/java/org/apache/dolphinscheduler/plugin/datasource/databend/param/DatabendDataSourceProcessorTest.java @@ -151,7 +151,7 @@ public void testGetJdbcUrl() { @Test public void testDbType() { Assertions.assertEquals(19, DbType.DATABEND.getCode()); - Assertions.assertEquals("databend", DbType.DATABEND.getDescp()); + Assertions.assertEquals("databend", DbType.DATABEND.getName()); Assertions.assertEquals(DbType.DATABEND, DbType.of(19)); } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-db2/src/main/java/org/apache/dolphinscheduler/plugin/datasource/db2/DB2DataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-db2/src/main/java/org/apache/dolphinscheduler/plugin/datasource/db2/DB2DataSourceChannelFactory.java index cda8a2e59259..3bbae238ea3b 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-db2/src/main/java/org/apache/dolphinscheduler/plugin/datasource/db2/DB2DataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-db2/src/main/java/org/apache/dolphinscheduler/plugin/datasource/db2/DB2DataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class DB2DataSourceChannelFactory implements DataSourceChannelFactory { @Override public String getName() { - return "db2"; + return DbType.DB2.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-doris/src/main/java/org/apache/dolphinscheduler/plugin/doris/DorisDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-doris/src/main/java/org/apache/dolphinscheduler/plugin/doris/DorisDataSourceChannelFactory.java index d663c362f24c..7180a6c6c254 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-doris/src/main/java/org/apache/dolphinscheduler/plugin/doris/DorisDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-doris/src/main/java/org/apache/dolphinscheduler/plugin/doris/DorisDataSourceChannelFactory.java @@ -32,6 +32,6 @@ public DataSourceChannel create() { @Override public String getName() { - return DbType.DORIS.getDescp(); + return DbType.DORIS.getName(); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hana/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hana/HanaDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hana/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hana/HanaDataSourceChannelFactory.java index 75aacebaff2e..91d275aab6b5 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hana/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hana/HanaDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hana/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hana/HanaDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class HanaDataSourceChannelFactory implements DataSourceChannelFactory { @Override public String getName() { - return "hana"; + return DbType.HANA.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/HiveDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/HiveDataSourceChannelFactory.java index 96ee007c8dbc..2caa4092dc11 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/HiveDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/HiveDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class HiveDataSourceChannelFactory implements DataSourceChannelFactory { @Override public String getName() { - return "hive"; + return DbType.HIVE.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-k8s/src/main/java/org/apache/dolphinscheduler/plugin/datasource/k8s/K8sDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-k8s/src/main/java/org/apache/dolphinscheduler/plugin/datasource/k8s/K8sDataSourceChannelFactory.java index 03ec046de84e..6a4428b47b24 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-k8s/src/main/java/org/apache/dolphinscheduler/plugin/datasource/k8s/K8sDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-k8s/src/main/java/org/apache/dolphinscheduler/plugin/datasource/k8s/K8sDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -32,7 +33,7 @@ public DataSourceChannel create() { @Override public String getName() { - return "k8s"; + return DbType.K8S.getName(); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-k8s/src/main/java/org/apache/dolphinscheduler/plugin/datasource/k8s/param/K8sDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-k8s/src/main/java/org/apache/dolphinscheduler/plugin/datasource/k8s/param/K8sDataSourceProcessor.java index 9e7342d433d9..fd3b49469fd6 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-k8s/src/main/java/org/apache/dolphinscheduler/plugin/datasource/k8s/param/K8sDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-k8s/src/main/java/org/apache/dolphinscheduler/plugin/datasource/k8s/param/K8sDataSourceProcessor.java @@ -58,7 +58,7 @@ public void checkDatasourceParam(BaseDataSourceParamDTO datasourceParam) { @Override public String getDatasourceUniqueId(ConnectionParam connectionParam, DbType dbType) { K8sConnectionParam baseConnectionParam = (K8sConnectionParam) connectionParam; - return MessageFormat.format("{0}@{1}@{2}", dbType.getDescp(), + return MessageFormat.format("{0}@{1}@{2}", dbType.getName(), PasswordUtils.encodePassword(baseConnectionParam.getKubeConfig()), baseConnectionParam.getNamespace()); } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-kyuubi/src/main/java/org/apache/dolphinscheduler/plugin/datasource/kyuubi/KyuubiDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-kyuubi/src/main/java/org/apache/dolphinscheduler/plugin/datasource/kyuubi/KyuubiDataSourceChannelFactory.java index 4c67a2098f8f..c60e74ccf846 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-kyuubi/src/main/java/org/apache/dolphinscheduler/plugin/datasource/kyuubi/KyuubiDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-kyuubi/src/main/java/org/apache/dolphinscheduler/plugin/datasource/kyuubi/KyuubiDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class KyuubiDataSourceChannelFactory implements DataSourceChannelFactory @Override public String getName() { - return "kyuubi"; + return DbType.KYUUBI.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-kyuubi/src/test/java/org/apache/dolphinscheduler/plugin/datasource/kyuubi/param/KyuubiDataSourceProcessorTest.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-kyuubi/src/test/java/org/apache/dolphinscheduler/plugin/datasource/kyuubi/param/KyuubiDataSourceProcessorTest.java index 865565c5dc0c..a18ceb4216d2 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-kyuubi/src/test/java/org/apache/dolphinscheduler/plugin/datasource/kyuubi/param/KyuubiDataSourceProcessorTest.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-kyuubi/src/test/java/org/apache/dolphinscheduler/plugin/datasource/kyuubi/param/KyuubiDataSourceProcessorTest.java @@ -143,7 +143,7 @@ public void testGetJdbcUrl() { @Test public void testDbType() { Assertions.assertEquals(18, DbType.KYUUBI.getCode()); - Assertions.assertEquals("kyuubi", DbType.KYUUBI.getDescp()); + Assertions.assertEquals("kyuubi", DbType.KYUUBI.getName()); Assertions.assertEquals(DbType.KYUUBI, DbType.of(18)); } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-mysql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/mysql/MySQLDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-mysql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/mysql/MySQLDataSourceChannelFactory.java index e57fc7e61d26..adc3ec79467f 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-mysql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/mysql/MySQLDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-mysql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/mysql/MySQLDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class MySQLDataSourceChannelFactory implements DataSourceChannelFactory { @Override public String getName() { - return "mysql"; + return DbType.MYSQL.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oceanbase/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oceanbase/OceanBaseDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oceanbase/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oceanbase/OceanBaseDataSourceChannelFactory.java index a69d6b3ae5ec..13650679b0bb 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oceanbase/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oceanbase/OceanBaseDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oceanbase/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oceanbase/OceanBaseDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class OceanBaseDataSourceChannelFactory implements DataSourceChannelFacto @Override public String getName() { - return "oceanbase"; + return DbType.OCEANBASE.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oracle/OracleDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oracle/OracleDataSourceChannelFactory.java index dedbce494693..f63aff9a2b49 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oracle/OracleDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-oracle/src/main/java/org/apache/dolphinscheduler/plugin/datasource/oracle/OracleDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class OracleDataSourceChannelFactory implements DataSourceChannelFactory @Override public String getName() { - return "oracle"; + return DbType.ORACLE.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/PostgreSQLDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/PostgreSQLDataSourceChannelFactory.java index 8aa6e566b781..e82a2e686052 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/PostgreSQLDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/PostgreSQLDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class PostgreSQLDataSourceChannelFactory implements DataSourceChannelFact @Override public String getName() { - return "postgresql"; + return DbType.POSTGRESQL.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-presto/src/main/java/org/apache/dolphinscheduler/plugin/datasource/presto/PrestoDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-presto/src/main/java/org/apache/dolphinscheduler/plugin/datasource/presto/PrestoDataSourceChannelFactory.java index ed1292ffc99e..76bf9d0808ed 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-presto/src/main/java/org/apache/dolphinscheduler/plugin/datasource/presto/PrestoDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-presto/src/main/java/org/apache/dolphinscheduler/plugin/datasource/presto/PrestoDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class PrestoDataSourceChannelFactory implements DataSourceChannelFactory @Override public String getName() { - return "presto"; + return DbType.PRESTO.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-redshift/src/main/java/org/apache/dolphinscheduler/plugin/datasource/redshift/RedshiftDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-redshift/src/main/java/org/apache/dolphinscheduler/plugin/datasource/redshift/RedshiftDataSourceChannelFactory.java index 25a587ae06c4..8c588f0b44e2 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-redshift/src/main/java/org/apache/dolphinscheduler/plugin/datasource/redshift/RedshiftDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-redshift/src/main/java/org/apache/dolphinscheduler/plugin/datasource/redshift/RedshiftDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -32,6 +33,6 @@ public DataSourceChannel create() { @Override public String getName() { - return "redshift"; + return DbType.REDSHIFT.getName(); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sagemaker/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sagemaker/SagemakerDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sagemaker/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sagemaker/SagemakerDataSourceChannelFactory.java index 04ab93f36f9d..245784361445 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sagemaker/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sagemaker/SagemakerDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sagemaker/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sagemaker/SagemakerDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -32,7 +33,7 @@ public DataSourceChannel create() { @Override public String getName() { - return "sagemaker"; + return DbType.SAGEMAKER.getName(); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sagemaker/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sagemaker/param/SagemakerDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sagemaker/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sagemaker/param/SagemakerDataSourceProcessor.java index 4239f45e5c66..7452ef8a14f5 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sagemaker/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sagemaker/param/SagemakerDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sagemaker/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sagemaker/param/SagemakerDataSourceProcessor.java @@ -57,7 +57,7 @@ public void checkDatasourceParam(BaseDataSourceParamDTO datasourceParamDTO) { @Override public String getDatasourceUniqueId(ConnectionParam connectionParam, DbType dbType) { SagemakerConnectionParam baseConnectionParam = (SagemakerConnectionParam) connectionParam; - return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getDescp(), + return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getName(), PasswordUtils.encodePassword(baseConnectionParam.getUserName()), PasswordUtils.encodePassword(baseConnectionParam.getPassword()), PasswordUtils.encodePassword(baseConnectionParam.getAwsRegion())); diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-snowflake/src/main/java/org/apache/dolphinscheduler/plugin/datasource/snowflake/SnowflakeDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-snowflake/src/main/java/org/apache/dolphinscheduler/plugin/datasource/snowflake/SnowflakeDataSourceChannelFactory.java index 0d0c97ecd68a..6bbfd7a6fba2 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-snowflake/src/main/java/org/apache/dolphinscheduler/plugin/datasource/snowflake/SnowflakeDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-snowflake/src/main/java/org/apache/dolphinscheduler/plugin/datasource/snowflake/SnowflakeDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class SnowflakeDataSourceChannelFactory implements DataSourceChannelFacto @Override public String getName() { - return "snowflake"; + return DbType.SNOWFLAKE.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-snowflake/src/test/java/org/apache/dolphinscheduler/plugin/datasource/snowflake/param/SnowflakeDataSourceProcessorTest.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-snowflake/src/test/java/org/apache/dolphinscheduler/plugin/datasource/snowflake/param/SnowflakeDataSourceProcessorTest.java index 54c5acf0f225..c60e70576fb8 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-snowflake/src/test/java/org/apache/dolphinscheduler/plugin/datasource/snowflake/param/SnowflakeDataSourceProcessorTest.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-snowflake/src/test/java/org/apache/dolphinscheduler/plugin/datasource/snowflake/param/SnowflakeDataSourceProcessorTest.java @@ -169,7 +169,7 @@ public void testCreateDatasourceParamDTO() { @Test public void testDbType() { Assertions.assertEquals(20, DbType.SNOWFLAKE.getCode()); - Assertions.assertEquals("snowflake", DbType.SNOWFLAKE.getDescp()); + Assertions.assertEquals("snowflake", DbType.SNOWFLAKE.getName()); Assertions.assertEquals(DbType.of(20), DbType.SNOWFLAKE); Assertions.assertEquals(DbType.ofName("SNOWFLAKE"), DbType.SNOWFLAKE); } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-spark/src/main/java/org/apache/dolphinscheduler/plugin/datasource/spark/SparkDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-spark/src/main/java/org/apache/dolphinscheduler/plugin/datasource/spark/SparkDataSourceChannelFactory.java index dbda3da5bdef..25f29ff21fe3 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-spark/src/main/java/org/apache/dolphinscheduler/plugin/datasource/spark/SparkDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-spark/src/main/java/org/apache/dolphinscheduler/plugin/datasource/spark/SparkDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class SparkDataSourceChannelFactory implements DataSourceChannelFactory { @Override public String getName() { - return "spark"; + return DbType.SPARK.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sqlserver/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sqlserver/SQLServerDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sqlserver/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sqlserver/SQLServerDataSourceChannelFactory.java index e76f520d1e19..f29cf6415e65 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sqlserver/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sqlserver/SQLServerDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-sqlserver/src/main/java/org/apache/dolphinscheduler/plugin/datasource/sqlserver/SQLServerDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class SQLServerDataSourceChannelFactory implements DataSourceChannelFacto @Override public String getName() { - return "sqlserver"; + return DbType.SQLSERVER.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannelFactory.java index 3195432703b1..9742c97e9bcc 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class SSHDataSourceChannelFactory implements DataSourceChannelFactory { @Override public String getName() { - return "ssh"; + return DbType.SSH.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceProcessor.java index 6bf0bed1b90a..1916edba3531 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceProcessor.java @@ -55,7 +55,7 @@ public void checkDatasourceParam(BaseDataSourceParamDTO datasourceParamDTO) { @Override public String getDatasourceUniqueId(ConnectionParam connectionParam, DbType dbType) { SSHConnectionParam baseConnectionParam = (SSHConnectionParam) connectionParam; - return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getDescp(), baseConnectionParam.getHost(), + return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getName(), baseConnectionParam.getHost(), baseConnectionParam.getUser(), PasswordUtils.encodePassword(baseConnectionParam.getPassword())); } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-starrocks/src/main/java/org/apache/dolphinscheduler/plugin/datasource/starrocks/StarRocksDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-starrocks/src/main/java/org/apache/dolphinscheduler/plugin/datasource/starrocks/StarRocksDataSourceChannelFactory.java index 50e248395253..82f78cff2102 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-starrocks/src/main/java/org/apache/dolphinscheduler/plugin/datasource/starrocks/StarRocksDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-starrocks/src/main/java/org/apache/dolphinscheduler/plugin/datasource/starrocks/StarRocksDataSourceChannelFactory.java @@ -33,6 +33,6 @@ public DataSourceChannel create() { @Override public String getName() { - return DbType.STARROCKS.getDescp(); + return DbType.STARROCKS.getName(); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-trino/src/main/java/org/apache/dolphinscheduler/plugin/datasource/trino/TrinoDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-trino/src/main/java/org/apache/dolphinscheduler/plugin/datasource/trino/TrinoDataSourceChannelFactory.java index 8c9605d791ef..36a3817fb0a4 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-trino/src/main/java/org/apache/dolphinscheduler/plugin/datasource/trino/TrinoDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-trino/src/main/java/org/apache/dolphinscheduler/plugin/datasource/trino/TrinoDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class TrinoDataSourceChannelFactory implements DataSourceChannelFactory { @Override public String getName() { - return "trino"; + return DbType.TRINO.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-vertica/src/main/java/org/apache/dolphinscheduler/plugin/datasource/vertica/VerticaDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-vertica/src/main/java/org/apache/dolphinscheduler/plugin/datasource/vertica/VerticaDataSourceChannelFactory.java index b507a207b477..44e151f2f272 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-vertica/src/main/java/org/apache/dolphinscheduler/plugin/datasource/vertica/VerticaDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-vertica/src/main/java/org/apache/dolphinscheduler/plugin/datasource/vertica/VerticaDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -27,7 +28,7 @@ public class VerticaDataSourceChannelFactory implements DataSourceChannelFactory @Override public String getName() { - return "vertica"; + return DbType.VERTICA.getName(); } @Override diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-zeppelin/src/main/java/org/apache/dolphinscheduler/plugin/datasource/zeppelin/ZeppelinDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-zeppelin/src/main/java/org/apache/dolphinscheduler/plugin/datasource/zeppelin/ZeppelinDataSourceChannelFactory.java index 692819cf788b..559ee558363e 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-zeppelin/src/main/java/org/apache/dolphinscheduler/plugin/datasource/zeppelin/ZeppelinDataSourceChannelFactory.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-zeppelin/src/main/java/org/apache/dolphinscheduler/plugin/datasource/zeppelin/ZeppelinDataSourceChannelFactory.java @@ -19,6 +19,7 @@ import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; import com.google.auto.service.AutoService; @@ -32,7 +33,7 @@ public DataSourceChannel create() { @Override public String getName() { - return "zeppelin"; + return DbType.ZEPPELIN.getName(); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-zeppelin/src/main/java/org/apache/dolphinscheduler/plugin/datasource/zeppelin/param/ZeppelinDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-zeppelin/src/main/java/org/apache/dolphinscheduler/plugin/datasource/zeppelin/param/ZeppelinDataSourceProcessor.java index 92077275adcf..88a913974e9f 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-zeppelin/src/main/java/org/apache/dolphinscheduler/plugin/datasource/zeppelin/param/ZeppelinDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-zeppelin/src/main/java/org/apache/dolphinscheduler/plugin/datasource/zeppelin/param/ZeppelinDataSourceProcessor.java @@ -56,7 +56,7 @@ public void checkDatasourceParam(BaseDataSourceParamDTO datasourceParamDTO) { @Override public String getDatasourceUniqueId(ConnectionParam connectionParam, DbType dbType) { ZeppelinConnectionParam baseConnectionParam = (ZeppelinConnectionParam) connectionParam; - return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getDescp(), baseConnectionParam.getRestEndpoint(), + return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getName(), baseConnectionParam.getRestEndpoint(), baseConnectionParam.getUsername(), PasswordUtils.encodePassword(baseConnectionParam.getPassword())); } diff --git a/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java b/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java index e7ebbeee0a2a..882b170e1190 100644 --- a/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java +++ b/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java @@ -28,42 +28,44 @@ public enum DbType { - MYSQL(0, "mysql"), - POSTGRESQL(1, "postgresql"), - HIVE(2, "hive"), - SPARK(3, "spark"), - CLICKHOUSE(4, "clickhouse"), - ORACLE(5, "oracle"), - SQLSERVER(6, "sqlserver"), - DB2(7, "db2"), - PRESTO(8, "presto"), - H2(9, "h2"), - REDSHIFT(10, "redshift"), - ATHENA(11, "athena"), - TRINO(12, "trino"), - STARROCKS(13, "starrocks"), - AZURESQL(14, "azuresql"), - DAMENG(15, "dameng"), - OCEANBASE(16, "oceanbase"), - SSH(17, "ssh"), - KYUUBI(18, "kyuubi"), - DATABEND(19, "databend"), - SNOWFLAKE(20, "snowflake"), - VERTICA(21, "vertica"), - HANA(22, "hana"), - DORIS(23, "doris"), - ZEPPELIN(24, "zeppelin"), - SAGEMAKER(25, "sagemaker"), + MYSQL(0, "mysql", "mysql"), + POSTGRESQL(1, "postgresql", "postgresql"), + HIVE(2, "hive", "hive"), + SPARK(3, "spark", "spark"), + CLICKHOUSE(4, "clickhouse", "clickhouse"), + ORACLE(5, "oracle", "oracle"), + SQLSERVER(6, "sqlserver", "sqlserver"), + DB2(7, "db2", "db2"), + PRESTO(8, "presto", "presto"), + H2(9, "h2", "h2"), + REDSHIFT(10, "redshift", "redshift"), + ATHENA(11, "athena", "athena"), + TRINO(12, "trino", "trino"), + STARROCKS(13, "starrocks", "starrocks"), + AZURESQL(14, "azuresql", "azuresql"), + DAMENG(15, "dameng", "dameng"), + OCEANBASE(16, "oceanbase", "oceanbase"), + SSH(17, "ssh", "ssh"), + KYUUBI(18, "kyuubi", "kyuubi"), + DATABEND(19, "databend", "databend"), + SNOWFLAKE(20, "snowflake", "snowflake"), + VERTICA(21, "vertica", "vertica"), + HANA(22, "hana", "hana"), + DORIS(23, "doris", "doris"), + ZEPPELIN(24, "zeppelin", "zeppelin"), + SAGEMAKER(25, "sagemaker", "sagemaker"), - K8S(26, "k8s"); + K8S(26, "k8s", "k8s"); private static final Map DB_TYPE_MAP = Arrays.stream(DbType.values()).collect(toMap(DbType::getCode, Functions.identity())); @EnumValue private final int code; + private final String name; private final String descp; - DbType(int code, String descp) { + DbType(int code, String name, String descp) { this.code = code; + this.name = name; this.descp = descp; } @@ -83,6 +85,10 @@ public int getCode() { return code; } + public String getName() { + return name; + } + public String getDescp() { return descp; } From 5a6c6c37f006291bc774ec44fe6d3d6793721195 Mon Sep 17 00:00:00 2001 From: calvin Date: Sun, 28 Apr 2024 18:47:20 +0800 Subject: [PATCH 055/165] [Improvement-15910][UI] Supposed to provide a default value for the custom parallelism when using the mode of parallel execution. (#15912) * worked out the issue * imrpove the parallism strategy * imrpove the parallism strategy * merge from dev --- .../src/locales/en_US/project.ts | 3 ++- .../src/locales/zh_CN/project.ts | 3 ++- .../definition/components/start-modal.tsx | 20 ++++++++++--------- .../definition/components/use-form.ts | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/dolphinscheduler-ui/src/locales/en_US/project.ts b/dolphinscheduler-ui/src/locales/en_US/project.ts index 7a3975252630..5e5ee4b2a98e 100644 --- a/dolphinscheduler-ui/src/locales/en_US/project.ts +++ b/dolphinscheduler-ui/src/locales/en_US/project.ts @@ -249,7 +249,8 @@ export default { delete_task_validate_dependent_tasks_desc: 'The downstream dependent tasks exists. You can not delete the task.', warning_delete_scheduler_dependent_tasks_desc: - 'The downstream dependent tasks exists. Are you sure to delete the scheduler?' + 'The downstream dependent tasks exists. Are you sure to delete the scheduler?', + warning_too_large_parallelism_number: 'The parallelism number is too large. It is better not to be over 10.' }, task: { on_line: 'Online', diff --git a/dolphinscheduler-ui/src/locales/zh_CN/project.ts b/dolphinscheduler-ui/src/locales/zh_CN/project.ts index 50e0a821ef12..85de0122fb82 100644 --- a/dolphinscheduler-ui/src/locales/zh_CN/project.ts +++ b/dolphinscheduler-ui/src/locales/zh_CN/project.ts @@ -246,7 +246,8 @@ export default { delete_task_validate_dependent_tasks_desc: '下游存在依赖,你不能删除该任务.', warning_delete_scheduler_dependent_tasks_desc: - '下游存在依赖, 删除定时可能会对下游任务产生影响. 你确定要删除该定时嘛?' + '下游存在依赖, 删除定时可能会对下游任务产生影响. 你确定要删除该定时嘛?', + warning_too_large_parallelism_number: '并行度设置太大了, 最好不要超过10.', }, task: { on_line: '线上', diff --git a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/start-modal.tsx b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/start-modal.tsx index 872b5158440e..8a4b07b4bce3 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/start-modal.tsx +++ b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/start-modal.tsx @@ -44,7 +44,8 @@ import { NSwitch, NCheckbox, NDatePicker, - NRadioButton + NRadioButton, + NInputNumber } from 'naive-ui' import { ArrowDownOutlined, @@ -75,7 +76,6 @@ export default defineComponent({ props, emits: ['update:show', 'update:row', 'updateList'], setup(props, ctx) { - const parallelismRef = ref(false) const { t } = useI18n() const route = useRoute() const { startState } = useForm() @@ -296,7 +296,6 @@ export default defineComponent({ return { t, showTaskDependType, - parallelismRef, hideModal, handleStart, generalWarningTypeListOptions, @@ -504,17 +503,20 @@ export default defineComponent({ 10 + } > - - {t('project.workflow.custom_parallelism')} - - )} diff --git a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-form.ts b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-form.ts index 32b02a788030..33dd7ba2418b 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-form.ts +++ b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-form.ts @@ -66,7 +66,7 @@ export const useForm = () => { tenantCode: 'default', environmentCode: null, startParams: null, - expectedParallelismNumber: '', + expectedParallelismNumber: '2', dryRun: 0, testFlag: 0, version: null, From 647cbae4002c0ab3758d57827460ad7125a2c853 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Mon, 29 Apr 2024 16:14:23 +0800 Subject: [PATCH 056/165] [DSIP-32][Master] Add command fetcher strategy for master fetch command (#15900) --- .../dolphinscheduler_env.sh | 1 - .../dolphinscheduler_env.sh | 1 - .../dolphinscheduler_env.sh | 1 - .../dolphinscheduler_env.sh | 1 - docs/docs/en/architecture/configuration.md | 4 +- .../en/guide/installation/pseudo-cluster.md | 1 - docs/docs/zh/architecture/configuration.md | 47 +++++----- .../zh/guide/installation/pseudo-cluster.md | 1 - .../dao/mapper/CommandMapper.java | 12 +-- .../dao/repository/BaseDao.java | 5 ++ .../dao/repository/CommandDao.java | 39 ++++++++ .../dolphinscheduler/dao/repository/IDao.java | 5 ++ .../dao/repository/impl/CommandDaoImpl.java | 41 +++++++++ .../dao/mapper/CommandMapper.xml | 6 +- .../dao/mapper/CommandMapperTest.java | 13 ++- .../repository/impl/CommandDaoImplTest.java | 88 +++++++++++++++++++ .../command/CommandFetcherConfiguration.java | 49 +++++++++++ .../master/command/ICommandFetcher.java | 36 ++++++++ .../command/IdSlotBasedCommandFetcher.java | 73 +++++++++++++++ .../master/config/CommandFetchStrategy.java | 63 +++++++++++++ .../server/master/config/MasterConfig.java | 12 +-- .../runner/MasterSchedulerBootstrap.java | 38 ++------ .../src/main/resources/application.yaml | 9 +- .../master/config/MasterConfigTest.java | 12 +++ .../src/test/resources/application.yaml | 9 +- .../service/command/CommandService.java | 11 --- .../service/command/CommandServiceImpl.java | 9 -- .../command/MessageServiceImplTest.java | 10 --- .../src/main/resources/application.yaml | 9 +- 29 files changed, 484 insertions(+), 122 deletions(-) create mode 100644 dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/CommandDao.java create mode 100644 dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/CommandDaoImpl.java create mode 100644 dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/CommandDaoImplTest.java create mode 100644 dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/CommandFetcherConfiguration.java create mode 100644 dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/ICommandFetcher.java create mode 100644 dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/IdSlotBasedCommandFetcher.java create mode 100644 dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/CommandFetchStrategy.java diff --git a/.github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh b/.github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh index 58937e740c14..8536eb0905a9 100755 --- a/.github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh +++ b/.github/workflows/cluster-test/mysql_with_mysql_registry/dolphinscheduler_env.sh @@ -28,7 +28,6 @@ export SPRING_DATASOURCE_PASSWORD=123456 # DolphinScheduler server related configuration export SPRING_CACHE_TYPE=${SPRING_CACHE_TYPE:-none} export SPRING_JACKSON_TIME_ZONE=${SPRING_JACKSON_TIME_ZONE:-UTC} -export MASTER_FETCH_COMMAND_NUM=${MASTER_FETCH_COMMAND_NUM:-10} # Registry center configuration, determines the type and link of the registry center export REGISTRY_TYPE=${REGISTRY_TYPE:-jdbc} diff --git a/.github/workflows/cluster-test/mysql_with_zookeeper_registry/dolphinscheduler_env.sh b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/dolphinscheduler_env.sh index 671c70a5bba5..f64e59b768c5 100755 --- a/.github/workflows/cluster-test/mysql_with_zookeeper_registry/dolphinscheduler_env.sh +++ b/.github/workflows/cluster-test/mysql_with_zookeeper_registry/dolphinscheduler_env.sh @@ -28,7 +28,6 @@ export SPRING_DATASOURCE_PASSWORD=123456 # DolphinScheduler server related configuration export SPRING_CACHE_TYPE=${SPRING_CACHE_TYPE:-none} export SPRING_JACKSON_TIME_ZONE=${SPRING_JACKSON_TIME_ZONE:-UTC} -export MASTER_FETCH_COMMAND_NUM=${MASTER_FETCH_COMMAND_NUM:-10} # Registry center configuration, determines the type and link of the registry center export REGISTRY_TYPE=${REGISTRY_TYPE:-zookeeper} diff --git a/.github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh index e7fd1b7204a5..29f8570319b1 100644 --- a/.github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh +++ b/.github/workflows/cluster-test/postgresql_with_postgresql_registry/dolphinscheduler_env.sh @@ -28,7 +28,6 @@ export SPRING_DATASOURCE_PASSWORD=postgres # DolphinScheduler server related configuration export SPRING_CACHE_TYPE=${SPRING_CACHE_TYPE:-none} export SPRING_JACKSON_TIME_ZONE=${SPRING_JACKSON_TIME_ZONE:-UTC} -export MASTER_FETCH_COMMAND_NUM=${MASTER_FETCH_COMMAND_NUM:-10} # Registry center configuration, determines the type and link of the registry center export REGISTRY_TYPE=jdbc diff --git a/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/dolphinscheduler_env.sh b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/dolphinscheduler_env.sh index 1dbd63254eee..685171605850 100644 --- a/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/dolphinscheduler_env.sh +++ b/.github/workflows/cluster-test/postgresql_with_zookeeper_registry/dolphinscheduler_env.sh @@ -28,7 +28,6 @@ export SPRING_DATASOURCE_PASSWORD=postgres # DolphinScheduler server related configuration export SPRING_CACHE_TYPE=${SPRING_CACHE_TYPE:-none} export SPRING_JACKSON_TIME_ZONE=${SPRING_JACKSON_TIME_ZONE:-UTC} -export MASTER_FETCH_COMMAND_NUM=${MASTER_FETCH_COMMAND_NUM:-10} # Registry center configuration, determines the type and link of the registry center export REGISTRY_TYPE=${REGISTRY_TYPE:-zookeeper} diff --git a/docs/docs/en/architecture/configuration.md b/docs/docs/en/architecture/configuration.md index 13d89329439b..fe0b7851bae1 100644 --- a/docs/docs/en/architecture/configuration.md +++ b/docs/docs/en/architecture/configuration.md @@ -286,7 +286,6 @@ Location: `master-server/conf/application.yaml` | Parameters | Default value | Description | |-----------------------------------------------------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | master.listen-port | 5678 | master listen port | -| master.fetch-command-num | 10 | the number of commands fetched by master | | master.pre-exec-threads | 10 | master prepare execute thread number to limit handle commands in parallel | | master.exec-threads | 100 | master execute thread number to limit process instances in parallel | | master.dispatch-task-number | 3 | master dispatch task number per batch | @@ -305,6 +304,9 @@ Location: `master-server/conf/application.yaml` | master.registry-disconnect-strategy.strategy | stop | Used when the master disconnect from registry, default value: stop. Optional values include stop, waiting | | master.registry-disconnect-strategy.max-waiting-time | 100s | Used when the master disconnect from registry, and the disconnect strategy is waiting, this config means the master will waiting to reconnect to registry in given times, and after the waiting times, if the master still cannot connect to registry, will stop itself, if the value is 0s, the Master will wait infinitely | | master.worker-group-refresh-interval | 10s | The interval to refresh worker group from db to memory | +| master.command-fetch-strategy.type | ID_SLOT_BASED | The command fetch strategy, only support `ID_SLOT_BASED` | +| master.command-fetch-strategy.config.id-step | 1 | The id auto incremental step of t_ds_command in db | +| master.command-fetch-strategy.config.fetch-size | 10 | The number of commands fetched by master | ### Worker Server related configuration diff --git a/docs/docs/en/guide/installation/pseudo-cluster.md b/docs/docs/en/guide/installation/pseudo-cluster.md index e63436f203bb..7a3b43b00ef2 100644 --- a/docs/docs/en/guide/installation/pseudo-cluster.md +++ b/docs/docs/en/guide/installation/pseudo-cluster.md @@ -123,7 +123,6 @@ export SPRING_DATASOURCE_PASSWORD={password} # DolphinScheduler server related configuration export SPRING_CACHE_TYPE=${SPRING_CACHE_TYPE:-none} export SPRING_JACKSON_TIME_ZONE=${SPRING_JACKSON_TIME_ZONE:-UTC} -export MASTER_FETCH_COMMAND_NUM=${MASTER_FETCH_COMMAND_NUM:-10} # Registry center configuration, determines the type and link of the registry center export REGISTRY_TYPE=${REGISTRY_TYPE:-zookeeper} diff --git a/docs/docs/zh/architecture/configuration.md b/docs/docs/zh/architecture/configuration.md index 08fded19e069..d8d1d42d1eb8 100644 --- a/docs/docs/zh/architecture/configuration.md +++ b/docs/docs/zh/architecture/configuration.md @@ -281,29 +281,30 @@ common.properties配置文件目前主要是配置hadoop/s3/yarn/applicationId 位置:`master-server/conf/application.yaml` -| 参数 | 默认值 | 描述 | -|-----------------------------------------------------------------------------|--------------|-----------------------------------------------------------------------------------------| -| master.listen-port | 5678 | master监听端口 | -| master.fetch-command-num | 10 | master拉取command数量 | -| master.pre-exec-threads | 10 | master准备执行任务的数量,用于限制并行的command | -| master.exec-threads | 100 | master工作线程数量,用于限制并行的流程实例数量 | -| master.dispatch-task-number | 3 | master每个批次的派发任务数量 | -| master.host-selector | lower_weight | master host选择器,用于选择合适的worker执行任务,可选值: random, round_robin, lower_weight | -| master.max-heartbeat-interval | 10s | master最大心跳间隔 | -| master.task-commit-retry-times | 5 | 任务重试次数 | -| master.task-commit-interval | 1000 | 任务提交间隔,单位为毫秒 | -| master.state-wheel-interval | 5 | 轮询检查状态时间 | -| master.server-load-protection.enabled | true | 是否开启系统保护策略 | -| master.server-load-protection.max-system-cpu-usage-percentage-thresholds | 0.7 | master最大系统cpu使用值,只有当前系统cpu使用值低于最大系统cpu使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统CPU | -| master.server-load-protection.max-jvm-cpu-usage-percentage-thresholds | 0.7 | master最大JVM cpu使用值,只有当前JVM cpu使用值低于最大JVM cpu使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的JVM CPU | -| master.server-load-protection.max-system-memory-usage-percentage-thresholds | 0.7 | master最大系统 内存使用值,只有当前系统内存使用值低于最大系统内存使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统内存 | -| master.server-load-protection.max-disk-usage-percentage-thresholds | 0.7 | master最大系统磁盘使用值,只有当前系统磁盘使用值低于最大系统磁盘使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统磁盘空间 | -| master.failover-interval | 10 | failover间隔,单位为分钟 | -| master.kill-application-when-task-failover | true | 当任务实例failover时,是否kill掉yarn或k8s application | -| master.registry-disconnect-strategy.strategy | stop | 当Master与注册中心失联之后采取的策略, 默认值是: stop. 可选值包括: stop, waiting | -| master.registry-disconnect-strategy.max-waiting-time | 100s | 当Master与注册中心失联之后重连时间, 之后当strategy为waiting时,该值生效。 该值表示当Master与注册中心失联时会在给定时间之内进行重连, | -| 在给定时间之内重连失败将会停止自己,在重连时,Master会丢弃目前正在执行的工作流,值为0表示会无限期等待 | -| master.master.worker-group-refresh-interval | 10s | 定期将workerGroup从数据库中同步到内存的时间间隔 | +| 参数 | 默认值 | 描述 | +|-----------------------------------------------------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------| +| master.listen-port | 5678 | master监听端口 | +| master.pre-exec-threads | 10 | master准备执行任务的数量,用于限制并行的command | +| master.exec-threads | 100 | master工作线程数量,用于限制并行的流程实例数量 | +| master.dispatch-task-number | 3 | master每个批次的派发任务数量 | +| master.host-selector | lower_weight | master host选择器,用于选择合适的worker执行任务,可选值: random, round_robin, lower_weight | +| master.max-heartbeat-interval | 10s | master最大心跳间隔 | +| master.task-commit-retry-times | 5 | 任务重试次数 | +| master.task-commit-interval | 1000 | 任务提交间隔,单位为毫秒 | +| master.state-wheel-interval | 5 | 轮询检查状态时间 | +| master.server-load-protection.enabled | true | 是否开启系统保护策略 | +| master.server-load-protection.max-system-cpu-usage-percentage-thresholds | 0.7 | master最大系统cpu使用值,只有当前系统cpu使用值低于最大系统cpu使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统CPU | +| master.server-load-protection.max-jvm-cpu-usage-percentage-thresholds | 0.7 | master最大JVM cpu使用值,只有当前JVM cpu使用值低于最大JVM cpu使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的JVM CPU | +| master.server-load-protection.max-system-memory-usage-percentage-thresholds | 0.7 | master最大系统 内存使用值,只有当前系统内存使用值低于最大系统内存使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统内存 | +| master.server-load-protection.max-disk-usage-percentage-thresholds | 0.7 | master最大系统磁盘使用值,只有当前系统磁盘使用值低于最大系统磁盘使用值,master服务才能调度任务. 默认值为0.7: 会使用70%的操作系统磁盘空间 | +| master.failover-interval | 10 | failover间隔,单位为分钟 | +| master.kill-application-when-task-failover | true | 当任务实例failover时,是否kill掉yarn或k8s application | +| master.registry-disconnect-strategy.strategy | stop | 当Master与注册中心失联之后采取的策略, 默认值是: stop. 可选值包括: stop, waiting | +| master.registry-disconnect-strategy.max-waiting-time | 100s | 当Master与注册中心失联之后重连时间, 之后当strategy为waiting时,该值生效。 该值表示当Master与注册中心失联时会在给定时间之内进行重连, 在给定时间之内重连失败将会停止自己,在重连时,Master会丢弃目前正在执行的工作流,值为0表示会无限期等待 | +| master.master.worker-group-refresh-interval | 10s | 定期将workerGroup从数据库中同步到内存的时间间隔 | +| master.command-fetch-strategy.type | ID_SLOT_BASED | Command拉取策略, 目前仅支持 `ID_SLOT_BASED` | +| master.command-fetch-strategy.config.id-step | 1 | 数据库中t_ds_command的id自增步长 | +| master.command-fetch-strategy.config.fetch-size | 10 | master拉取command数量 | ## Worker Server相关配置 diff --git a/docs/docs/zh/guide/installation/pseudo-cluster.md b/docs/docs/zh/guide/installation/pseudo-cluster.md index 13479e0d9e05..a199167e0445 100644 --- a/docs/docs/zh/guide/installation/pseudo-cluster.md +++ b/docs/docs/zh/guide/installation/pseudo-cluster.md @@ -118,7 +118,6 @@ export SPRING_DATASOURCE_PASSWORD={password} # DolphinScheduler server related configuration export SPRING_CACHE_TYPE=${SPRING_CACHE_TYPE:-none} export SPRING_JACKSON_TIME_ZONE=${SPRING_JACKSON_TIME_ZONE:-UTC} -export MASTER_FETCH_COMMAND_NUM=${MASTER_FETCH_COMMAND_NUM:-10} # Registry center configuration, determines the type and link of the registry center export REGISTRY_TYPE=${REGISTRY_TYPE:-zookeeper} diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/CommandMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/CommandMapper.java index 9fb664322764..8c8314e7ccaa 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/CommandMapper.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/CommandMapper.java @@ -52,14 +52,10 @@ List countCommandState( */ List queryCommandPage(@Param("limit") int limit, @Param("offset") int offset); - /** - * query command page by slot - * - * @return command list - */ - List queryCommandPageBySlot(@Param("limit") int limit, - @Param("masterCount") int masterCount, - @Param("thisMasterSlot") int thisMasterSlot); + List queryCommandByIdSlot(@Param("currentSlotIndex") int currentSlotIndex, + @Param("totalSlot") int totalSlot, + @Param("idStep") int idStep, + @Param("fetchNumber") int fetchNum); void deleteByWorkflowInstanceIds(@Param("workflowInstanceIds") List workflowInstanceIds); } diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/BaseDao.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/BaseDao.java index 2937957dbdc4..664b56ee472c 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/BaseDao.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/BaseDao.java @@ -56,6 +56,11 @@ public List queryByIds(Collection ids) { return mybatisMapper.selectBatchIds(ids); } + @Override + public List queryAll() { + return mybatisMapper.selectList(null); + } + @Override public List queryByCondition(ENTITY queryCondition) { if (queryCondition == null) { diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/CommandDao.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/CommandDao.java new file mode 100644 index 000000000000..daa52b83181d --- /dev/null +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/CommandDao.java @@ -0,0 +1,39 @@ +/* + * 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.dao.repository; + +import org.apache.dolphinscheduler.dao.entity.Command; + +import java.util.List; + +public interface CommandDao extends IDao { + + /** + * Query command by command id and server slot, return the command which match (commandId / step) %s totalSlot = currentSlotIndex + * + * @param currentSlotIndex current slot index + * @param totalSlot total slot number + * @param idStep id step in db + * @param fetchNum fetch number + * @return command list + */ + List queryCommandByIdSlot(int currentSlotIndex, + int totalSlot, + int idStep, + int fetchNum); +} diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/IDao.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/IDao.java index c566d9b90402..ab774196003f 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/IDao.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/IDao.java @@ -41,6 +41,11 @@ public interface IDao { */ List queryByIds(Collection ids); + /** + * Query all entities. + */ + List queryAll(); + /** * Query the entity by condition. */ diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/CommandDaoImpl.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/CommandDaoImpl.java new file mode 100644 index 000000000000..0b510d15b51d --- /dev/null +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/CommandDaoImpl.java @@ -0,0 +1,41 @@ +/* + * 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.dao.repository.impl; + +import org.apache.dolphinscheduler.dao.entity.Command; +import org.apache.dolphinscheduler.dao.mapper.CommandMapper; +import org.apache.dolphinscheduler.dao.repository.BaseDao; +import org.apache.dolphinscheduler.dao.repository.CommandDao; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +@Repository +public class CommandDaoImpl extends BaseDao implements CommandDao { + + public CommandDaoImpl(CommandMapper commandMapper) { + super(commandMapper); + } + + @Override + public List queryCommandByIdSlot(int currentSlotIndex, int totalSlot, int idStep, int fetchNum) { + return mybatisMapper.queryCommandByIdSlot(currentSlotIndex, totalSlot, idStep, fetchNum); + } + +} diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/CommandMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/CommandMapper.xml index 56db890ef07a..16f7c05f2534 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/CommandMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/CommandMapper.xml @@ -40,12 +40,12 @@ limit #{limit} offset #{offset} - select * from t_ds_command - where id % #{masterCount} = #{thisMasterSlot} + where (id / #{idStep}) % #{totalSlot} = #{currentSlotIndex} order by process_instance_priority, id asc - limit #{limit} + limit #{fetchNumber} delete from t_ds_command diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/CommandMapperTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/CommandMapperTest.java index 2d367e46e4c4..560b68754a58 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/CommandMapperTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/CommandMapperTest.java @@ -187,7 +187,7 @@ private boolean toTestQueryCommandPageBySlot(int masterCount, int thisMasterSlot Command command = createCommand(); Integer id = command.getId(); boolean hit = id % masterCount == thisMasterSlot; - List commandList = commandMapper.queryCommandPageBySlot(1, masterCount, thisMasterSlot); + List commandList = commandMapper.queryCommandByIdSlot(thisMasterSlot, masterCount, 1, 1); if (hit) { Assertions.assertEquals(id, commandList.get(0).getId()); } else { @@ -201,8 +201,9 @@ private boolean toTestQueryCommandPageBySlot(int masterCount, int thisMasterSlot /** * create command map - * @param count map count - * @param commandType comman type + * + * @param count map count + * @param commandType comman type * @param processDefinitionCode process definition code * @return command map */ @@ -223,7 +224,8 @@ private CommandCount createCommandMap( } /** - * create process definition + * create process definition + * * @return process definition */ private ProcessDefinition createProcessDefinition() { @@ -243,6 +245,7 @@ private ProcessDefinition createProcessDefinition() { /** * create command map + * * @param count map count * @return command map */ @@ -258,6 +261,7 @@ private Map createCommandMap(Integer count) { /** * create command + * * @return */ private Command createCommand() { @@ -266,6 +270,7 @@ private Command createCommand() { /** * create command + * * @return Command */ private Command createCommand(CommandType commandType, long processDefinitionCode) { diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/CommandDaoImplTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/CommandDaoImplTest.java new file mode 100644 index 000000000000..85867ef3b591 --- /dev/null +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/CommandDaoImplTest.java @@ -0,0 +1,88 @@ +/* + * 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.dao.repository.impl; + +import static com.google.common.truth.Truth.assertThat; + +import org.apache.dolphinscheduler.common.constants.Constants; +import org.apache.dolphinscheduler.common.enums.CommandType; +import org.apache.dolphinscheduler.common.enums.FailureStrategy; +import org.apache.dolphinscheduler.common.enums.Priority; +import org.apache.dolphinscheduler.common.enums.TaskDependType; +import org.apache.dolphinscheduler.common.enums.WarningType; +import org.apache.dolphinscheduler.common.utils.DateUtils; +import org.apache.dolphinscheduler.dao.BaseDaoTest; +import org.apache.dolphinscheduler.dao.entity.Command; +import org.apache.dolphinscheduler.dao.repository.CommandDao; + +import org.apache.commons.lang3.RandomUtils; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CommandDaoImplTest extends BaseDaoTest { + + @Autowired + private CommandDao commandDao; + + @Test + void fetchCommandByIdSlot() { + int commandSize = RandomUtils.nextInt(1, 1000); + for (int i = 0; i < commandSize; i++) { + createCommand(CommandType.START_PROCESS, 0); + } + int totalSlot = RandomUtils.nextInt(1, 10); + int currentSlotIndex = RandomUtils.nextInt(0, totalSlot); + int fetchSize = RandomUtils.nextInt(10, 100); + for (int i = 1; i < 5; i++) { + int idStep = i; + List commands = commandDao.queryCommandByIdSlot(currentSlotIndex, totalSlot, idStep, fetchSize); + assertThat(commands.size()).isGreaterThan(0); + assertThat(commands.size()) + .isEqualTo(commandDao.queryAll() + .stream() + .filter(command -> (command.getId() / idStep) % totalSlot == currentSlotIndex) + .limit(fetchSize) + .count()); + + } + + } + + private void createCommand(CommandType commandType, int processDefinitionCode) { + Command command = new Command(); + command.setCommandType(commandType); + command.setProcessDefinitionCode(processDefinitionCode); + command.setExecutorId(4); + command.setCommandParam("test command param"); + command.setTaskDependType(TaskDependType.TASK_ONLY); + command.setFailureStrategy(FailureStrategy.CONTINUE); + command.setWarningType(WarningType.ALL); + command.setWarningGroupId(1); + command.setScheduleTime(DateUtils.stringToDate("2019-12-29 12:10:00")); + command.setProcessInstancePriority(Priority.MEDIUM); + command.setStartTime(DateUtils.stringToDate("2019-12-29 10:10:00")); + command.setUpdateTime(DateUtils.stringToDate("2019-12-29 10:10:00")); + command.setWorkerGroup(Constants.DEFAULT_WORKER_GROUP); + command.setProcessInstanceId(0); + command.setProcessDefinitionVersion(0); + commandDao.insert(command); + } +} diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/CommandFetcherConfiguration.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/CommandFetcherConfiguration.java new file mode 100644 index 000000000000..4a4d3c1efc65 --- /dev/null +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/CommandFetcherConfiguration.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.server.master.command; + +import static com.google.common.base.Preconditions.checkNotNull; + +import org.apache.dolphinscheduler.dao.repository.CommandDao; +import org.apache.dolphinscheduler.server.master.config.CommandFetchStrategy; +import org.apache.dolphinscheduler.server.master.config.MasterConfig; +import org.apache.dolphinscheduler.server.master.registry.MasterSlotManager; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CommandFetcherConfiguration { + + @Bean + public ICommandFetcher commandFetcher(MasterConfig masterConfig, + MasterSlotManager masterSlotManager, + CommandDao commandDao) { + CommandFetchStrategy commandFetchStrategy = + checkNotNull(masterConfig.getCommandFetchStrategy(), "command fetch strategy is null"); + switch (commandFetchStrategy.getType()) { + case ID_SLOT_BASED: + CommandFetchStrategy.IdSlotBasedFetchConfig idSlotBasedFetchConfig = + (CommandFetchStrategy.IdSlotBasedFetchConfig) commandFetchStrategy.getConfig(); + return new IdSlotBasedCommandFetcher(idSlotBasedFetchConfig, masterSlotManager, commandDao); + default: + throw new IllegalArgumentException( + "unsupported command fetch strategy type: " + commandFetchStrategy.getType()); + } + } +} diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/ICommandFetcher.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/ICommandFetcher.java new file mode 100644 index 000000000000..c315a9b29497 --- /dev/null +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/ICommandFetcher.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.dolphinscheduler.server.master.command; + +import org.apache.dolphinscheduler.dao.entity.Command; + +import java.util.List; + +/** + * The command fetcher used to fetch commands + */ +public interface ICommandFetcher { + + /** + * Fetch commands + * + * @return command list which need to be handled + */ + List fetchCommands(); + +} diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/IdSlotBasedCommandFetcher.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/IdSlotBasedCommandFetcher.java new file mode 100644 index 000000000000..a4178200938e --- /dev/null +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/command/IdSlotBasedCommandFetcher.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.server.master.command; + +import org.apache.dolphinscheduler.dao.entity.Command; +import org.apache.dolphinscheduler.dao.repository.CommandDao; +import org.apache.dolphinscheduler.server.master.config.CommandFetchStrategy; +import org.apache.dolphinscheduler.server.master.metrics.ProcessInstanceMetrics; +import org.apache.dolphinscheduler.server.master.registry.MasterSlotManager; + +import java.util.Collections; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * The command fetcher which is fetch commands by command id and slot. + */ +@Slf4j +public class IdSlotBasedCommandFetcher implements ICommandFetcher { + + private final CommandFetchStrategy.IdSlotBasedFetchConfig idSlotBasedFetchConfig; + + private final CommandDao commandDao; + + private final MasterSlotManager masterSlotManager; + + public IdSlotBasedCommandFetcher(CommandFetchStrategy.IdSlotBasedFetchConfig idSlotBasedFetchConfig, + MasterSlotManager masterSlotManager, + CommandDao commandDao) { + this.idSlotBasedFetchConfig = idSlotBasedFetchConfig; + this.masterSlotManager = masterSlotManager; + this.commandDao = commandDao; + } + + @Override + public List fetchCommands() { + long scheduleStartTime = System.currentTimeMillis(); + int currentSlotIndex = masterSlotManager.getSlot(); + int totalSlot = masterSlotManager.getMasterSize(); + if (totalSlot <= 0 || currentSlotIndex < 0) { + log.warn("Slot is validated, current master slots: {}, the current slot index is {}", totalSlot, + currentSlotIndex); + return Collections.emptyList(); + } + List commands = commandDao.queryCommandByIdSlot( + currentSlotIndex, + totalSlot, + idSlotBasedFetchConfig.getIdStep(), + idSlotBasedFetchConfig.getFetchSize()); + long cost = System.currentTimeMillis() - scheduleStartTime; + log.info("Fetch commands: {} success, cost: {}ms, totalSlot: {}, currentSlotIndex: {}", commands.size(), cost, + totalSlot, currentSlotIndex); + ProcessInstanceMetrics.recordCommandQueryTime(cost); + return commands; + } + +} diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/CommandFetchStrategy.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/CommandFetchStrategy.java new file mode 100644 index 000000000000..e61941677c65 --- /dev/null +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/CommandFetchStrategy.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.server.master.config; + +import lombok.Data; + +import org.springframework.validation.Errors; + +@Data +public class CommandFetchStrategy { + + private CommandFetchStrategyType type = CommandFetchStrategyType.ID_SLOT_BASED; + + private CommandFetchConfig config = new IdSlotBasedFetchConfig(); + + public void validate(Errors errors) { + config.validate(errors); + } + + public enum CommandFetchStrategyType { + ID_SLOT_BASED, + ; + } + + public interface CommandFetchConfig { + + void validate(Errors errors); + + } + + @Data + public static class IdSlotBasedFetchConfig implements CommandFetchConfig { + + private int idStep = 1; + private int fetchSize = 10; + + @Override + public void validate(Errors errors) { + if (idStep <= 0) { + errors.rejectValue("step", null, "step must be greater than 0"); + } + if (fetchSize <= 0) { + errors.rejectValue("fetchSize", null, "fetchSize must be greater than 0"); + } + } + } + +} diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/MasterConfig.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/MasterConfig.java index 02c0dcb819d6..20d3cccef3e2 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/MasterConfig.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/config/MasterConfig.java @@ -48,10 +48,6 @@ public class MasterConfig implements Validator { * The master RPC server listen port. */ private int listenPort = 5678; - /** - * The max batch size used to fetch command from database. - */ - private int fetchCommandNum = 10; /** * The thread number used to prepare processInstance. This number shouldn't bigger than fetchCommandNum. */ @@ -98,6 +94,8 @@ public class MasterConfig implements Validator { private Duration workerGroupRefreshInterval = Duration.ofSeconds(10L); + private CommandFetchStrategy commandFetchStrategy = new CommandFetchStrategy(); + // ip:listenPort private String masterAddress; @@ -115,9 +113,6 @@ public void validate(Object target, Errors errors) { if (masterConfig.getListenPort() <= 0) { errors.rejectValue("listen-port", null, "is invalidated"); } - if (masterConfig.getFetchCommandNum() <= 0) { - errors.rejectValue("fetch-command-num", null, "should be a positive value"); - } if (masterConfig.getPreExecThreads() <= 0) { errors.rejectValue("per-exec-threads", null, "should be a positive value"); } @@ -149,6 +144,7 @@ public void validate(Object target, Errors errors) { if (StringUtils.isEmpty(masterConfig.getMasterAddress())) { masterConfig.setMasterAddress(NetUtils.getAddr(masterConfig.getListenPort())); } + commandFetchStrategy.validate(errors); masterConfig.setMasterRegistryPath( RegistryNodeType.MASTER.getRegistryPath() + "/" + masterConfig.getMasterAddress()); @@ -159,7 +155,6 @@ private void printConfig() { String config = "\n****************************Master Configuration**************************************" + "\n listen-port -> " + listenPort + - "\n fetch-command-num -> " + fetchCommandNum + "\n pre-exec-threads -> " + preExecThreads + "\n exec-threads -> " + execThreads + "\n dispatch-task-number -> " + dispatchTaskNumber + @@ -175,6 +170,7 @@ private void printConfig() { "\n master-address -> " + masterAddress + "\n master-registry-path: " + masterRegistryPath + "\n worker-group-refresh-interval: " + workerGroupRefreshInterval + + "\n command-fetch-strategy: " + commandFetchStrategy + "\n****************************Master Configuration**************************************"; log.info(config); } diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/MasterSchedulerBootstrap.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/MasterSchedulerBootstrap.java index 2fddd9438474..c1b5d0ffabfd 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/MasterSchedulerBootstrap.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/MasterSchedulerBootstrap.java @@ -26,21 +26,18 @@ import org.apache.dolphinscheduler.meter.metrics.MetricsProvider; import org.apache.dolphinscheduler.meter.metrics.SystemMetrics; import org.apache.dolphinscheduler.server.master.cache.ProcessInstanceExecCacheManager; +import org.apache.dolphinscheduler.server.master.command.ICommandFetcher; import org.apache.dolphinscheduler.server.master.config.MasterConfig; import org.apache.dolphinscheduler.server.master.config.MasterServerLoadProtection; import org.apache.dolphinscheduler.server.master.event.WorkflowEvent; import org.apache.dolphinscheduler.server.master.event.WorkflowEventQueue; import org.apache.dolphinscheduler.server.master.event.WorkflowEventType; -import org.apache.dolphinscheduler.server.master.exception.MasterException; import org.apache.dolphinscheduler.server.master.exception.WorkflowCreateException; import org.apache.dolphinscheduler.server.master.metrics.MasterServerMetrics; -import org.apache.dolphinscheduler.server.master.metrics.ProcessInstanceMetrics; -import org.apache.dolphinscheduler.server.master.registry.MasterSlotManager; import org.apache.dolphinscheduler.service.command.CommandService; import org.apache.commons.collections4.CollectionUtils; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -56,6 +53,9 @@ @Slf4j public class MasterSchedulerBootstrap extends BaseDaemonThread implements AutoCloseable { + @Autowired + private ICommandFetcher commandFetcher; + @Autowired private CommandService commandService; @@ -74,9 +74,6 @@ public class MasterSchedulerBootstrap extends BaseDaemonThread implements AutoCl @Autowired private WorkflowEventLooper workflowEventLooper; - @Autowired - private MasterSlotManager masterSlotManager; - @Autowired private MasterTaskExecutorBootstrap masterTaskExecutorBootstrap; @@ -125,7 +122,7 @@ public void run() { Thread.sleep(Constants.SLEEP_TIME_MILLIS); continue; } - List commands = findCommands(); + List commands = commandFetcher.fetchCommands(); if (CollectionUtils.isEmpty(commands)) { // indicate that no command ,sleep for 1s Thread.sleep(Constants.SLEEP_TIME_MILLIS); @@ -170,29 +167,4 @@ public void run() { } } - private List findCommands() throws MasterException { - try { - long scheduleStartTime = System.currentTimeMillis(); - int thisMasterSlot = masterSlotManager.getSlot(); - int masterCount = masterSlotManager.getMasterSize(); - if (masterCount <= 0) { - log.warn("Master count: {} is invalid, the current slot: {}", masterCount, thisMasterSlot); - return Collections.emptyList(); - } - int pageSize = masterConfig.getFetchCommandNum(); - final List result = - commandService.findCommandPageBySlot(pageSize, masterCount, thisMasterSlot); - if (CollectionUtils.isNotEmpty(result)) { - long cost = System.currentTimeMillis() - scheduleStartTime; - log.info( - "Master schedule bootstrap loop command success, fetch command size: {}, cost: {}ms, current slot: {}, total slot size: {}", - result.size(), cost, thisMasterSlot, masterCount); - ProcessInstanceMetrics.recordCommandQueryTime(cost); - } - return result; - } catch (Exception ex) { - throw new MasterException("Master loop command from database error", ex); - } - } - } diff --git a/dolphinscheduler-master/src/main/resources/application.yaml b/dolphinscheduler-master/src/main/resources/application.yaml index f18c6ef61db3..17b1e41a7109 100644 --- a/dolphinscheduler-master/src/main/resources/application.yaml +++ b/dolphinscheduler-master/src/main/resources/application.yaml @@ -83,8 +83,6 @@ registry: master: listen-port: 5678 - # master fetch command num - fetch-command-num: 10 # master prepare execute thread number to limit handle commands in parallel pre-exec-threads: 10 # master execute thread number to limit process instances in parallel @@ -121,6 +119,13 @@ master: # The max waiting time to reconnect to registry if you set the strategy to waiting max-waiting-time: 100s worker-group-refresh-interval: 10s + command-fetch-strategy: + type: ID_SLOT_BASED + config: + # The incremental id step + id-step: 1 + # master fetch command num + fetch-size: 10 server: port: 5679 diff --git a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterConfigTest.java b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterConfigTest.java index faab44cf854c..9d26aa81f4bb 100644 --- a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterConfigTest.java +++ b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/config/MasterConfigTest.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.server.master.config; +import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -47,6 +48,17 @@ public void getServerLoadProtection() { assertEquals(0.77, serverLoadProtection.getMaxJvmCpuUsagePercentageThresholds()); assertEquals(0.77, serverLoadProtection.getMaxSystemMemoryUsagePercentageThresholds()); assertEquals(0.77, serverLoadProtection.getMaxDiskUsagePercentageThresholds()); + } + + @Test + public void getCommandFetchStrategy() { + CommandFetchStrategy commandFetchStrategy = masterConfig.getCommandFetchStrategy(); + assertThat(commandFetchStrategy.getType()) + .isEqualTo(CommandFetchStrategy.CommandFetchStrategyType.ID_SLOT_BASED); + CommandFetchStrategy.IdSlotBasedFetchConfig idSlotBasedFetchConfig = + (CommandFetchStrategy.IdSlotBasedFetchConfig) commandFetchStrategy.getConfig(); + assertThat(idSlotBasedFetchConfig.getIdStep()).isEqualTo(3); + assertThat(idSlotBasedFetchConfig.getFetchSize()).isEqualTo(11); } } diff --git a/dolphinscheduler-master/src/test/resources/application.yaml b/dolphinscheduler-master/src/test/resources/application.yaml index f4827d4b3c56..15f91996090a 100644 --- a/dolphinscheduler-master/src/test/resources/application.yaml +++ b/dolphinscheduler-master/src/test/resources/application.yaml @@ -89,8 +89,6 @@ registry: master: listen-port: 5678 - # master fetch command num - fetch-command-num: 10 # master prepare execute thread number to limit handle commands in parallel pre-exec-threads: 10 # master execute thread number to limit process instances in parallel @@ -127,6 +125,13 @@ master: # The max waiting time to reconnect to registry if you set the strategy to waiting max-waiting-time: 100s worker-group-refresh-interval: 10s + command-fetch-strategy: + type: ID_SLOT_BASED + config: + # The incremental id step + id-step: 3 + # master fetch command num + fetch-size: 11 server: port: 5679 diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/command/CommandService.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/command/CommandService.java index cff73c503f2c..43b81c4e5c45 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/command/CommandService.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/command/CommandService.java @@ -22,8 +22,6 @@ import org.apache.dolphinscheduler.dao.entity.ProcessInstanceMap; import org.apache.dolphinscheduler.dao.entity.TaskInstance; -import java.util.List; - /** * Command Service */ @@ -44,15 +42,6 @@ public interface CommandService { */ int createCommand(Command command); - /** - * Get command page - * @param pageSize page size - * @param masterCount master count - * @param thisMasterSlot master slot - * @return command page - */ - List findCommandPageBySlot(int pageSize, int masterCount, int thisMasterSlot); - /** * check the input command exists in queue list * diff --git a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/command/CommandServiceImpl.java b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/command/CommandServiceImpl.java index 483899446b2a..ee833a80b045 100644 --- a/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/command/CommandServiceImpl.java +++ b/dolphinscheduler-service/src/main/java/org/apache/dolphinscheduler/service/command/CommandServiceImpl.java @@ -57,7 +57,6 @@ import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.Lists; import io.micrometer.core.annotation.Counted; /** @@ -107,14 +106,6 @@ public int createCommand(Command command) { return result; } - @Override - public List findCommandPageBySlot(int pageSize, int masterCount, int thisMasterSlot) { - if (masterCount <= 0) { - return Lists.newArrayList(); - } - return commandMapper.queryCommandPageBySlot(pageSize, masterCount, thisMasterSlot); - } - @Override public boolean verifyIsNeedCreateCommand(Command command) { boolean isNeedCreate = true; diff --git a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/command/MessageServiceImplTest.java b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/command/MessageServiceImplTest.java index 0cde76bdfe88..f60320fc63e6 100644 --- a/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/command/MessageServiceImplTest.java +++ b/dolphinscheduler-service/src/test/java/org/apache/dolphinscheduler/service/command/MessageServiceImplTest.java @@ -214,14 +214,4 @@ public void testCreateCommand() { Mockito.verify(commandMapper, Mockito.times(1)).insert(command); } - @Test - public void testFindCommandPageBySlot() { - int pageSize = 1; - int masterCount = 0; - int thisMasterSlot = 2; - List commandList = - commandService.findCommandPageBySlot(pageSize, masterCount, thisMasterSlot); - Assertions.assertEquals(0, commandList.size()); - } - } diff --git a/dolphinscheduler-standalone-server/src/main/resources/application.yaml b/dolphinscheduler-standalone-server/src/main/resources/application.yaml index 5122eea2a17c..6757718929de 100644 --- a/dolphinscheduler-standalone-server/src/main/resources/application.yaml +++ b/dolphinscheduler-standalone-server/src/main/resources/application.yaml @@ -160,8 +160,6 @@ casdoor: master: listen-port: 5678 - # master fetch command num - fetch-command-num: 10 # master prepare execute thread number to limit handle commands in parallel pre-exec-threads: 10 # master execute thread number to limit process instances in parallel @@ -192,6 +190,13 @@ master: # kill yarn/k8s application when failover taskInstance, default true kill-application-when-task-failover: true worker-group-refresh-interval: 10s + command-fetch-strategy: + type: ID_SLOT_BASED + config: + # The incremental id step + id-step: 1 + # master fetch command num + fetch-size: 10 worker: # worker listener port From 0a11cd21bd0e73b52d4e68dae2f32519031e2e50 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Mon, 29 Apr 2024 18:07:05 +0800 Subject: [PATCH 057/165] Fix jar path is not correct in java task (#15906) --- .../task/api/resource/ResourceContext.java | 1 - .../plugin/task/hivecli/HiveCliTaskTest.java | 4 +- .../plugin/task/java/JavaTask.java | 41 ++----------------- .../plugin/task/java/JavaTaskTest.java | 32 ++++++++------- .../utils/TaskExecutionContextUtils.java | 1 - 5 files changed, 23 insertions(+), 56 deletions(-) diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/resource/ResourceContext.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/resource/ResourceContext.java index 687d1aeb958b..f90b5269024c 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/resource/ResourceContext.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/resource/ResourceContext.java @@ -60,7 +60,6 @@ public ResourceItem getResourceItem(String resourceAbsolutePathInStorage) { public static class ResourceItem { private String resourceAbsolutePathInStorage; - private String resourceRelativePath; private String resourceAbsolutePathInLocal; } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-hivecli/src/test/java/org/apache/dolphinscheduler/plugin/task/hivecli/HiveCliTaskTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-hivecli/src/test/java/org/apache/dolphinscheduler/plugin/task/hivecli/HiveCliTaskTest.java index 824ad49f899b..b4136af3c317 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-hivecli/src/test/java/org/apache/dolphinscheduler/plugin/task/hivecli/HiveCliTaskTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-hivecli/src/test/java/org/apache/dolphinscheduler/plugin/task/hivecli/HiveCliTaskTest.java @@ -65,7 +65,7 @@ public void after() { } @Test - public void hiveCliTaskExecuteSqlFromScript() throws Exception { + public void hiveCliTaskExecuteSqlFromScript() { String hiveCliTaskParameters = buildHiveCliTaskExecuteSqlFromScriptParameters(); HiveCliTask hiveCliTask = prepareHiveCliTaskForTest(hiveCliTaskParameters); hiveCliTask.init(); @@ -78,7 +78,7 @@ public void hiveCliTaskExecuteSqlFromFile() { TaskExecutionContext taskExecutionContext = new TaskExecutionContext(); taskExecutionContext.setTaskParams(hiveCliTaskParameters); ResourceContext resourceContext = new ResourceContext(); - resourceContext.addResourceItem(new ResourceContext.ResourceItem("/sql_tasks/hive_task.sql", "123_node.sql", + resourceContext.addResourceItem(new ResourceContext.ResourceItem("/sql_tasks/hive_task.sql", "/sql_tasks/hive_task.sql")); taskExecutionContext.setResourceContext(resourceContext); diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-java/src/main/java/org/apache/dolphinscheduler/plugin/task/java/JavaTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-java/src/main/java/org/apache/dolphinscheduler/plugin/task/java/JavaTask.java index 179b50c35cab..fc2326034548 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-java/src/main/java/org/apache/dolphinscheduler/plugin/task/java/JavaTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-java/src/main/java/org/apache/dolphinscheduler/plugin/task/java/JavaTask.java @@ -88,6 +88,7 @@ public JavaTask(TaskExecutionContext taskRequest) { /** * Initializes a Java task + * * @return void **/ @Override @@ -178,7 +179,8 @@ protected String buildJavaCommand() throws Exception { **/ protected String buildJarCommand() { ResourceContext resourceContext = taskRequest.getResourceContext(); - String mainJarName = resourceContext.getResourceItem(javaParameters.getMainJar().getResourceName()) + String mainJarAbsolutePathInLocal = resourceContext + .getResourceItem(javaParameters.getMainJar().getResourceName()) .getResourceAbsolutePathInLocal(); StringBuilder builder = new StringBuilder(); builder.append(getJavaCommandPath()) @@ -186,7 +188,7 @@ protected String buildJarCommand() { .append(buildResourcePath()).append(" ") .append("-jar").append(" ") .append(taskRequest.getExecutePath()).append(FOLDER_SEPARATOR) - .append(mainJarName).append(" ") + .append(mainJarAbsolutePathInLocal).append(" ") .append(javaParameters.getMainArgs().trim()).append(" ") .append(javaParameters.getJvmArgs().trim()); return builder.toString(); @@ -207,39 +209,6 @@ public AbstractParameters getParameters() { return javaParameters; } - /** - * Replaces placeholders such as local variables in source files - * - * @param rawScript - * @return String - * @throws StringIndexOutOfBoundsException - */ - protected static String convertJavaSourceCodePlaceholders(String rawScript) throws StringIndexOutOfBoundsException { - int len = "${setShareVar(${".length(); - - int scriptStart = 0; - while ((scriptStart = rawScript.indexOf("${setShareVar(${", scriptStart)) != -1) { - int start = -1; - int end = rawScript.indexOf('}', scriptStart + len); - String prop = rawScript.substring(scriptStart + len, end); - - start = rawScript.indexOf(',', end); - end = rawScript.indexOf(')', start); - - String value = rawScript.substring(start + 1, end); - - start = rawScript.indexOf('}', start) + 1; - end = rawScript.length(); - - String replaceScript = String.format("print(\"${{setValue({},{})}}\".format(\"%s\",%s))", prop, value); - - rawScript = rawScript.substring(0, scriptStart) + replaceScript + rawScript.substring(start, end); - - scriptStart += replaceScript.length(); - } - return rawScript; - } - /** * Creates a Java source file when it does not exist * @@ -290,8 +259,6 @@ protected String buildResourcePath() { for (ResourceInfo info : javaParameters.getResourceFilesList()) { builder.append(JavaConstants.PATH_SEPARATOR); builder - .append(taskRequest.getExecutePath()) - .append(FOLDER_SEPARATOR) .append(resourceContext.getResourceItem(info.getResourceName()).getResourceAbsolutePathInLocal()); } return builder.toString(); diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-java/src/test/java/org/apache/dolphinscheduler/plugin/task/java/JavaTaskTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-java/src/test/java/org/apache/dolphinscheduler/plugin/task/java/JavaTaskTest.java index 55756241ce10..c82941532614 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-java/src/test/java/org/apache/dolphinscheduler/plugin/task/java/JavaTaskTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-java/src/test/java/org/apache/dolphinscheduler/plugin/task/java/JavaTaskTest.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.plugin.task.java; +import static com.google.common.truth.Truth.assertThat; import static org.apache.dolphinscheduler.plugin.task.api.enums.DataType.VARCHAR; import static org.apache.dolphinscheduler.plugin.task.api.enums.Direct.IN; import static org.apache.dolphinscheduler.plugin.task.java.JavaConstants.RUN_TYPE_JAR; @@ -34,7 +35,6 @@ import org.apache.dolphinscheduler.plugin.task.java.exception.PublicClassNotFoundException; import org.apache.dolphinscheduler.plugin.task.java.exception.RunTypeNotFoundException; -import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Files; @@ -82,10 +82,10 @@ public void testGetPubllicClassName() { **/ @Test public void buildJarCommand() { - String homeBinPath = JavaConstants.JAVA_HOME_VAR + File.separator + "bin" + File.separator; JavaTask javaTask = runJarType(); - Assertions.assertEquals(javaTask.buildJarCommand(), homeBinPath - + "java -classpath .:/tmp/dolphinscheduler/test/executepath:/tmp/dolphinscheduler/test/executepath/opt/share/jar/resource2.jar -jar /tmp/dolphinscheduler/test/executepath/opt/share/jar/main.jar -host 127.0.0.1 -port 8080 -xms:50m"); + assertThat(javaTask.buildJarCommand()) + .isEqualTo( + "${JAVA_HOME}/bin/java -classpath .:/tmp/dolphinscheduler/test/executepath:opt/share/jar/resource2.jar -jar /tmp/dolphinscheduler/test/executepath/opt/share/jar/main.jar -host 127.0.0.1 -port 8080 -xms:50m"); } /** @@ -101,14 +101,13 @@ public void buildJavaCompileCommand() throws IOException { Assertions.assertEquals("JavaTaskTest", publicClassName); String fileName = javaTask.buildJavaSourceCodeFileFullName(publicClassName); try { - String homeBinPath = JavaConstants.JAVA_HOME_VAR + File.separator + "bin" + File.separator; Path path = Paths.get(fileName); if (Files.exists(path)) { Files.delete(path); } - Assertions.assertEquals(homeBinPath - + "javac -classpath .:/tmp/dolphinscheduler/test/executepath:/tmp/dolphinscheduler/test/executepath/opt/share/jar/resource2.jar /tmp/dolphinscheduler/test/executepath/JavaTaskTest.java", - javaTask.buildJavaCompileCommand(sourceCode)); + assertThat(javaTask.buildJavaCompileCommand(sourceCode)) + .isEqualTo( + "${JAVA_HOME}/bin/javac -classpath .:/tmp/dolphinscheduler/test/executepath:opt/share/jar/resource2.jar /tmp/dolphinscheduler/test/executepath/JavaTaskTest.java"); } finally { Path path = Paths.get(fileName); if (Files.exists(path)) { @@ -121,26 +120,29 @@ public void buildJavaCompileCommand() throws IOException { /** * Construct java to run the command * - * @return void + * @return void **/ @Test public void buildJavaCommand() throws Exception { - String wantJavaCommand = - "${JAVA_HOME}/bin/javac -classpath .:/tmp/dolphinscheduler/test/executepath:/tmp/dolphinscheduler/test/executepath/opt/share/jar/resource2.jar /tmp/dolphinscheduler/test/executepath/JavaTaskTest.java;${JAVA_HOME}/bin/java -classpath .:/tmp/dolphinscheduler/test/executepath:/tmp/dolphinscheduler/test/executepath/opt/share/jar/resource2.jar JavaTaskTest -host 127.0.0.1 -port 8080 -xms:50m"; JavaTask javaTask = runJavaType(); String sourceCode = javaTask.buildJavaSourceContent(); String publicClassName = javaTask.getPublicClassName(sourceCode); + Assertions.assertEquals("JavaTaskTest", publicClassName); + String fileName = javaTask.buildJavaSourceCodeFileFullName(publicClassName); Path path = Paths.get(fileName); if (Files.exists(path)) { Files.delete(path); } - Assertions.assertEquals(wantJavaCommand, javaTask.buildJavaCommand()); + assertThat(javaTask.buildJavaCommand()) + .isEqualTo( + "${JAVA_HOME}/bin/javac -classpath .:/tmp/dolphinscheduler/test/executepath:opt/share/jar/resource2.jar /tmp/dolphinscheduler/test/executepath/JavaTaskTest.java;${JAVA_HOME}/bin/java -classpath .:/tmp/dolphinscheduler/test/executepath:opt/share/jar/resource2.jar JavaTaskTest -host 127.0.0.1 -port 8080 -xms:50m"); } /** * There is no exception to overwriting the Java source file + * * @return void * @throws IOException **/ @@ -259,8 +261,8 @@ public JavaTask runJavaType() { resourceItem2.setResourceAbsolutePathInLocal("opt/share/jar/main.jar"); ResourceContext.ResourceItem resourceItem3 = new ResourceContext.ResourceItem(); - resourceItem2.setResourceAbsolutePathInStorage("/JavaTaskTest.java"); - resourceItem2.setResourceAbsolutePathInLocal("JavaTaskTest.java"); + resourceItem3.setResourceAbsolutePathInStorage("/JavaTaskTest.java"); + resourceItem3.setResourceAbsolutePathInLocal("JavaTaskTest.java"); ResourceContext resourceContext = new ResourceContext(); resourceContext.addResourceItem(resourceItem1); @@ -275,7 +277,7 @@ public JavaTask runJavaType() { /** * The Java task to construct the jar run mode * - * @return JavaTask + * @return JavaTask **/ private JavaTask runJarType() { TaskExecutionContext taskExecutionContext = new TaskExecutionContext(); diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/utils/TaskExecutionContextUtils.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/utils/TaskExecutionContextUtils.java index f43b19c4444c..8d83dde593b7 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/utils/TaskExecutionContextUtils.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/utils/TaskExecutionContextUtils.java @@ -151,7 +151,6 @@ public static ResourceContext downloadResourcesIfNeeded(String tenant, } ResourceContext.ResourceItem resourceItem = ResourceContext.ResourceItem.builder() .resourceAbsolutePathInStorage(resourceAbsolutePathInStorage) - .resourceRelativePath(resourceRelativePath) .resourceAbsolutePathInLocal(resourceAbsolutePathInLocal) .build(); resourceContext.addResourceItem(resourceItem); From ebcdaeb9ac125a13b74ec3f057693816a6758426 Mon Sep 17 00:00:00 2001 From: Evan Sun Date: Mon, 29 Apr 2024 21:03:01 +0800 Subject: [PATCH 058/165] [TEST] increase coverage of project preference service test (#15939) --- .../service/ProjectPreferenceServiceTest.java | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectPreferenceServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectPreferenceServiceTest.java index 530c15d48e61..7a74a7c26547 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectPreferenceServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectPreferenceServiceTest.java @@ -60,28 +60,65 @@ public class ProjectPreferenceServiceTest { public void testUpdateProjectPreference() { User loginUser = getGeneralUser(); + // no permission + Mockito.when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + .thenReturn(false); + Result result = projectPreferenceService.updateProjectPreference(loginUser, projectCode, "value"); + Assertions.assertNull(result.getCode()); + Assertions.assertNull(result.getData()); + Assertions.assertNull(result.getMsg()); + + // when preference exists in project + Mockito.when(projectPreferenceMapper.selectOne(Mockito.any())).thenReturn(null); Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); + + // success Mockito.when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) .thenReturn(true); - Mockito.when(projectPreferenceMapper.selectOne(Mockito.any())).thenReturn(null); Mockito.when(projectPreferenceMapper.insert(Mockito.any())).thenReturn(1); - Result result = projectPreferenceService.updateProjectPreference(loginUser, projectCode, "value"); + result = projectPreferenceService.updateProjectPreference(loginUser, projectCode, "value"); + Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + + // database operatation fail + Mockito.when(projectPreferenceMapper.insert(Mockito.any())).thenReturn(-1); + result = projectPreferenceService.updateProjectPreference(loginUser, projectCode, "value"); + Assertions.assertEquals(Status.CREATE_PROJECT_PREFERENCE_ERROR.getCode(), result.getCode()); + + // when preference exists in project + Mockito.when(projectPreferenceMapper.selectOne(Mockito.any())).thenReturn(getProjectPreference()); + + // success + Mockito.when(projectPreferenceMapper.updateById(Mockito.any())).thenReturn(1); + result = projectPreferenceService.updateProjectPreference(loginUser, projectCode, "value"); Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + + // database operation fail + Mockito.when(projectPreferenceMapper.updateById(Mockito.any())).thenReturn(-1); + result = projectPreferenceService.updateProjectPreference(loginUser, projectCode, "value"); + Assertions.assertEquals(Status.UPDATE_PROJECT_PREFERENCE_ERROR.getCode(), result.getCode()); } @Test public void testQueryProjectPreferenceByProjectCode() { User loginUser = getGeneralUser(); + // no permission + Mockito.when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + .thenReturn(false); + Result result = projectPreferenceService.queryProjectPreferenceByProjectCode(loginUser, projectCode); + Assertions.assertNull(result.getCode()); + Assertions.assertNull(result.getData()); + Assertions.assertNull(result.getMsg()); + // PROJECT_PARAMETER_NOT_EXISTS Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); Mockito.when(projectService.hasProjectAndPerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class), Mockito.any())).thenReturn(true); Mockito.when(projectPreferenceMapper.selectOne(Mockito.any())).thenReturn(null); - Result result = projectPreferenceService.queryProjectPreferenceByProjectCode(loginUser, projectCode); + result = projectPreferenceService.queryProjectPreferenceByProjectCode(loginUser, projectCode); Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); // SUCCESS @@ -94,14 +131,29 @@ public void testQueryProjectPreferenceByProjectCode() { public void testEnableProjectPreference() { User loginUser = getGeneralUser(); + // no permission + Mockito.when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) + .thenReturn(false); + Result result = projectPreferenceService.enableProjectPreference(loginUser, projectCode, 1); + Assertions.assertNull(result.getCode()); + Assertions.assertNull(result.getData()); + Assertions.assertNull(result.getMsg()); + Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode)); Mockito.when(projectService.hasProjectAndWritePerm(Mockito.any(), Mockito.any(), Mockito.any(Result.class))) .thenReturn(true); + // success Mockito.when(projectPreferenceMapper.selectOne(Mockito.any())).thenReturn(getProjectPreference()); - Result result = projectPreferenceService.enableProjectPreference(loginUser, projectCode, 1); + Mockito.when(projectPreferenceMapper.updateById(Mockito.any())).thenReturn(1); + result = projectPreferenceService.enableProjectPreference(loginUser, projectCode, 2); Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + // db operation fail + Mockito.when(projectPreferenceMapper.selectOne(Mockito.any())).thenReturn(getProjectPreference()); + Mockito.when(projectPreferenceMapper.updateById(Mockito.any())).thenReturn(-1); + result = projectPreferenceService.enableProjectPreference(loginUser, projectCode, 2); + Assertions.assertEquals(Status.UPDATE_PROJECT_PREFERENCE_STATE_ERROR.getCode(), result.getCode()); } private User getGeneralUser() { From fa6ea8ba7b06e21177114058b1f1c32fe005a60a Mon Sep 17 00:00:00 2001 From: Evan Sun Date: Mon, 6 May 2024 10:02:04 +0800 Subject: [PATCH 059/165] [TEST] increase coverage of project workergroup relation service (#15944) Co-authored-by: abzymeinsjtu --- ...ProjectWorkerGroupRelationServiceImpl.java | 12 +- ...ProjectWorkerGroupRelationServiceTest.java | 106 ++++++++++++++---- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectWorkerGroupRelationServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectWorkerGroupRelationServiceImpl.java index 15cbcbb7fa86..31dc113dbb1d 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectWorkerGroupRelationServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ProjectWorkerGroupRelationServiceImpl.java @@ -26,6 +26,7 @@ import org.apache.dolphinscheduler.dao.entity.Project; import org.apache.dolphinscheduler.dao.entity.ProjectWorkerGroup; import org.apache.dolphinscheduler.dao.entity.User; +import org.apache.dolphinscheduler.dao.entity.WorkerGroup; import org.apache.dolphinscheduler.dao.mapper.ProjectMapper; import org.apache.dolphinscheduler.dao.mapper.ProjectWorkerGroupMapper; import org.apache.dolphinscheduler.dao.mapper.ScheduleMapper; @@ -38,6 +39,7 @@ import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -113,23 +115,25 @@ public Result assignWorkerGroupsToProject(User loginUser, Long projectCode, List } Set workerGroupNames = - workerGroupMapper.queryAllWorkerGroup().stream().map(item -> item.getName()).collect( + workerGroupMapper.queryAllWorkerGroup().stream().map(WorkerGroup::getName).collect( Collectors.toSet()); workerGroupNames.add(Constants.DEFAULT_WORKER_GROUP); - Set assignedWorkerGroupNames = workerGroups.stream().collect(Collectors.toSet()); + Set assignedWorkerGroupNames = new HashSet<>(workerGroups); Set difference = SetUtils.difference(assignedWorkerGroupNames, workerGroupNames); - if (difference.size() > 0) { + if (!difference.isEmpty()) { putMsg(result, Status.WORKER_GROUP_NOT_EXIST, difference.toString()); return result; } Set projectWorkerGroupNames = projectWorkerGroupMapper.selectList(new QueryWrapper() .lambda() - .eq(ProjectWorkerGroup::getProjectCode, projectCode)).stream().map(item -> item.getWorkerGroup()) + .eq(ProjectWorkerGroup::getProjectCode, projectCode)) + .stream() + .map(ProjectWorkerGroup::getWorkerGroup) .collect(Collectors.toSet()); difference = SetUtils.difference(projectWorkerGroupNames, assignedWorkerGroupNames); diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectWorkerGroupRelationServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectWorkerGroupRelationServiceTest.java index a8e8b8beb2f2..6f6b7ca2d6c1 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectWorkerGroupRelationServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ProjectWorkerGroupRelationServiceTest.java @@ -17,13 +17,17 @@ package org.apache.dolphinscheduler.api.service; +import static org.apache.dolphinscheduler.api.utils.ServiceTestUtil.getAdminUser; +import static org.apache.dolphinscheduler.api.utils.ServiceTestUtil.getGeneralUser; + +import org.apache.dolphinscheduler.api.AssertionsHelper; import org.apache.dolphinscheduler.api.enums.Status; import org.apache.dolphinscheduler.api.service.impl.ProjectWorkerGroupRelationServiceImpl; import org.apache.dolphinscheduler.api.utils.Result; import org.apache.dolphinscheduler.common.constants.Constants; -import org.apache.dolphinscheduler.common.enums.UserType; import org.apache.dolphinscheduler.dao.entity.Project; import org.apache.dolphinscheduler.dao.entity.ProjectWorkerGroup; +import org.apache.dolphinscheduler.dao.entity.TaskDefinition; import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.dao.entity.WorkerGroup; import org.apache.dolphinscheduler.dao.mapper.ProjectMapper; @@ -33,6 +37,7 @@ import org.apache.dolphinscheduler.dao.mapper.WorkerGroupMapper; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -77,27 +82,87 @@ public class ProjectWorkerGroupRelationServiceTest { @Test public void testAssignWorkerGroupsToProject() { + User generalUser = getGeneralUser(); User loginUser = getAdminUser(); + // no permission + Result result = projectWorkerGroupRelationService.assignWorkerGroupsToProject(generalUser, projectCode, + getWorkerGroups()); + Assertions.assertEquals(Status.USER_NO_OPERATION_PERM.getCode(), result.getCode()); + + // project code is null + result = projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, null, + getWorkerGroups()); + Assertions.assertEquals(Status.PROJECT_NOT_EXIST.getCode(), result.getCode()); + + // worker group is empty + result = projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, + Collections.emptyList()); + Assertions.assertEquals(Status.WORKER_GROUP_TO_PROJECT_IS_EMPTY.getCode(), result.getCode()); + + // project not exists Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(null); - Result result = projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, + result = projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, getWorkerGroups()); Assertions.assertEquals(Status.PROJECT_NOT_EXIST.getCode(), result.getCode()); + // worker group not exists WorkerGroup workerGroup = new WorkerGroup(); workerGroup.setName("test"); Mockito.when(projectMapper.queryByCode(Mockito.anyLong())).thenReturn(getProject()); - Mockito.when(workerGroupMapper.queryAllWorkerGroup()).thenReturn(Lists.newArrayList(workerGroup)); + Mockito.when(workerGroupMapper.queryAllWorkerGroup()).thenReturn(Collections.singletonList(workerGroup)); + result = projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, + getDiffWorkerGroups()); + Assertions.assertEquals(Status.WORKER_GROUP_NOT_EXIST.getCode(), result.getCode()); + + // db insertion fail + Mockito.when(workerGroupMapper.queryAllWorkerGroup()).thenReturn(Collections.singletonList(workerGroup)); + Mockito.when(projectWorkerGroupMapper.insert(Mockito.any())).thenReturn(-1); + AssertionsHelper.assertThrowsServiceException(Status.ASSIGN_WORKER_GROUP_TO_PROJECT_ERROR, + () -> projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, + getWorkerGroups())); + + // success Mockito.when(projectWorkerGroupMapper.insert(Mockito.any())).thenReturn(1); result = projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, getWorkerGroups()); Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + + // success when there is diff between current wg and assigned wg + Mockito.when(projectWorkerGroupMapper.selectList(Mockito.any())) + .thenReturn(Collections.singletonList(getDiffProjectWorkerGroup())); + Mockito.when(projectWorkerGroupMapper.delete(Mockito.any())).thenReturn(1); + result = projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, + getWorkerGroups()); + Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode()); + + // db deletion fail + Mockito.when(projectWorkerGroupMapper.delete(Mockito.any())).thenReturn(-1); + AssertionsHelper.assertThrowsServiceException(Status.ASSIGN_WORKER_GROUP_TO_PROJECT_ERROR, + () -> projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, + getWorkerGroups())); + + // fail when wg is referenced by task definition + Mockito.when(taskDefinitionMapper.queryAllDefinitionList(Mockito.anyLong())) + .thenReturn(Collections.singletonList(getTaskDefinitionWithDiffWorkerGroup())); + AssertionsHelper.assertThrowsServiceException(Status.USED_WORKER_GROUP_EXISTS, + () -> projectWorkerGroupRelationService.assignWorkerGroupsToProject(loginUser, projectCode, + getWorkerGroups())); } @Test public void testQueryWorkerGroupsByProject() { + // no permission + Mockito.when(projectService.hasProjectAndPerm(Mockito.any(), Mockito.any(), Mockito.anyMap(), Mockito.any())) + .thenReturn(false); + Map result = + projectWorkerGroupRelationService.queryWorkerGroupsByProject(getGeneralUser(), projectCode); + + Assertions.assertTrue(result.isEmpty()); + + // success Mockito.when(projectService.hasProjectAndPerm(Mockito.any(), Mockito.any(), Mockito.anyMap(), Mockito.any())) .thenReturn(true); @@ -113,8 +178,7 @@ public void testQueryWorkerGroupsByProject() { Mockito.when(scheduleMapper.querySchedulerListByProjectName(Mockito.any())) .thenReturn(Lists.newArrayList()); - Map result = - projectWorkerGroupRelationService.queryWorkerGroupsByProject(getGeneralUser(), projectCode); + result = projectWorkerGroupRelationService.queryWorkerGroupsByProject(getGeneralUser(), projectCode); ProjectWorkerGroup[] actualValue = ((List) result.get(Constants.DATA_LIST)).toArray(new ProjectWorkerGroup[0]); @@ -126,20 +190,8 @@ private List getWorkerGroups() { return Lists.newArrayList("default"); } - private User getGeneralUser() { - User loginUser = new User(); - loginUser.setUserType(UserType.GENERAL_USER); - loginUser.setUserName("userName"); - loginUser.setId(1); - return loginUser; - } - - private User getAdminUser() { - User loginUser = new User(); - loginUser.setUserType(UserType.ADMIN_USER); - loginUser.setUserName("userName"); - loginUser.setId(1); - return loginUser; + private List getDiffWorkerGroups() { + return Lists.newArrayList("default", "new"); } private Project getProject() { @@ -158,4 +210,20 @@ private ProjectWorkerGroup getProjectWorkerGroup() { projectWorkerGroup.setWorkerGroup("default"); return projectWorkerGroup; } + + private ProjectWorkerGroup getDiffProjectWorkerGroup() { + ProjectWorkerGroup projectWorkerGroup = new ProjectWorkerGroup(); + projectWorkerGroup.setId(2); + projectWorkerGroup.setProjectCode(projectCode); + projectWorkerGroup.setWorkerGroup("new"); + return projectWorkerGroup; + } + + private TaskDefinition getTaskDefinitionWithDiffWorkerGroup() { + TaskDefinition taskDefinition = new TaskDefinition(); + taskDefinition.setProjectCode(projectCode); + taskDefinition.setId(1); + taskDefinition.setWorkerGroup("new"); + return taskDefinition; + } } From d218b021e7e045e7982a3c91ea7b1b1491d21d38 Mon Sep 17 00:00:00 2001 From: JohnHuang Date: Mon, 6 May 2024 11:33:16 +0800 Subject: [PATCH 060/165] [DSIP-25][Remote Logging] Split remote logging configuration (#15826) Co-authored-by: Rick Cheng --- .../dolphinscheduler-alert-server.xml | 1 + .../assembly/dolphinscheduler-api-server.xml | 1 + .../ImmutablePriorityPropertyDelegate.java | 33 ++++++-- .../config/ImmutablePropertyDelegate.java | 2 +- .../common/config/ImmutableYamlDelegate.java | 82 +++++++++++++++++++ .../common/constants/Constants.java | 5 +- .../common/utils/PropertyUtils.java | 10 ++- .../src/main/resources/remote-logging.yaml | 61 ++++++++++++++ ...ImmutablePriorityPropertyDelegateTest.java | 5 +- .../src/test/resources/remote-logging.yaml | 61 ++++++++++++++ .../dolphinscheduler-master-server.xml | 1 + .../dolphinscheduler-worker-server.xml | 1 + 12 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutableYamlDelegate.java create mode 100644 dolphinscheduler-common/src/main/resources/remote-logging.yaml create mode 100644 dolphinscheduler-common/src/test/resources/remote-logging.yaml diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/assembly/dolphinscheduler-alert-server.xml b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/assembly/dolphinscheduler-alert-server.xml index 6f3909622133..bf28193b3fc9 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/assembly/dolphinscheduler-alert-server.xml +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/assembly/dolphinscheduler-alert-server.xml @@ -52,6 +52,7 @@ ${basedir}/../../dolphinscheduler-common/src/main/resources **/*.properties + **/*.yaml conf diff --git a/dolphinscheduler-api/src/main/assembly/dolphinscheduler-api-server.xml b/dolphinscheduler-api/src/main/assembly/dolphinscheduler-api-server.xml index 77b2f54c64a2..c9fdab4d51ed 100644 --- a/dolphinscheduler-api/src/main/assembly/dolphinscheduler-api-server.xml +++ b/dolphinscheduler-api/src/main/assembly/dolphinscheduler-api-server.xml @@ -53,6 +53,7 @@ ${basedir}/../dolphinscheduler-common/src/main/resources **/*.properties + **/*.yaml conf diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutablePriorityPropertyDelegate.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutablePriorityPropertyDelegate.java index 742e745fe40f..620f74ef95cf 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutablePriorityPropertyDelegate.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutablePriorityPropertyDelegate.java @@ -31,12 +31,18 @@ * This class will get the property by the priority of the following: env > jvm > properties. */ @Slf4j -public class ImmutablePriorityPropertyDelegate extends ImmutablePropertyDelegate { +public class ImmutablePriorityPropertyDelegate implements IPropertyDelegate { private static final Map>> configValueMap = new ConcurrentHashMap<>(); - public ImmutablePriorityPropertyDelegate(String propertyAbsolutePath) { - super(propertyAbsolutePath); + private ImmutablePropertyDelegate immutablePropertyDelegate; + + private ImmutableYamlDelegate immutableYamlDelegate; + + public ImmutablePriorityPropertyDelegate(ImmutablePropertyDelegate immutablePropertyDelegate, + ImmutableYamlDelegate immutableYamlDelegate) { + this.immutablePropertyDelegate = immutablePropertyDelegate; + this.immutableYamlDelegate = immutableYamlDelegate; } @Override @@ -56,8 +62,14 @@ public String get(String key) { return value; } value = getConfigValueFromProperties(key); + if (value.isPresent()) { + log.debug("Get config value from properties, key: {} actualKey: {}, value: {}", + k, value.get().getActualKey(), value.get().getValue()); + return value; + } + value = getConfigValueFromYaml(key); value.ifPresent( - stringConfigValue -> log.debug("Get config value from properties, key: {} actualKey: {}, value: {}", + stringConfigValue -> log.debug("Get config value from yaml, key: {} actualKey: {}, value: {}", k, stringConfigValue.getActualKey(), stringConfigValue.getValue())); return value; }); @@ -76,7 +88,8 @@ public String get(String key, String defaultValue) { @Override public Set getPropertyKeys() { Set propertyKeys = new HashSet<>(); - propertyKeys.addAll(super.getPropertyKeys()); + propertyKeys.addAll(this.immutablePropertyDelegate.getPropertyKeys()); + propertyKeys.addAll(this.immutableYamlDelegate.getPropertyKeys()); propertyKeys.addAll(System.getProperties().stringPropertyNames()); propertyKeys.addAll(System.getenv().keySet()); return propertyKeys; @@ -104,7 +117,15 @@ private Optional> getConfigValueFromJvm(String key) { } private Optional> getConfigValueFromProperties(String key) { - String value = super.get(key); + String value = this.immutablePropertyDelegate.get(key); + if (value != null) { + return Optional.of(ConfigValue.fromProperties(key, value)); + } + return Optional.empty(); + } + + private Optional> getConfigValueFromYaml(String key) { + String value = this.immutableYamlDelegate.get(key); if (value != null) { return Optional.of(ConfigValue.fromProperties(key, value)); } diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutablePropertyDelegate.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutablePropertyDelegate.java index b58735afb067..4a0c192210be 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutablePropertyDelegate.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutablePropertyDelegate.java @@ -49,7 +49,7 @@ public ImmutablePropertyDelegate(String... propertyAbsolutePath) { } catch (IOException e) { log.error("Load property: {} error, please check if the file exist under classpath", propertyAbsolutePath, e); - System.exit(1); + throw new RuntimeException(e); } } printProperties(); diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutableYamlDelegate.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutableYamlDelegate.java new file mode 100644 index 000000000000..5806a20fd7f7 --- /dev/null +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/config/ImmutableYamlDelegate.java @@ -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. + */ + +package org.apache.dolphinscheduler.common.config; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.io.InputStreamResource; + +@Slf4j +public class ImmutableYamlDelegate implements IPropertyDelegate { + + private static final String REMOTE_LOGGING_YAML_NAME = "/remote-logging.yaml"; + + private final Properties properties; + + public ImmutableYamlDelegate() { + this(REMOTE_LOGGING_YAML_NAME); + } + + public ImmutableYamlDelegate(String... yamlAbsolutePath) { + properties = new Properties(); + // read from classpath + for (String fileName : yamlAbsolutePath) { + try (InputStream fis = ImmutableYamlDelegate.class.getResourceAsStream(fileName)) { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new InputStreamResource(fis)); + factory.afterPropertiesSet(); + Properties subProperties = factory.getObject(); + properties.putAll(subProperties); + } catch (IOException e) { + log.error("Load property: {} error, please check if the file exist under classpath", + yamlAbsolutePath, e); + throw new RuntimeException(e); + } + } + printProperties(); + } + + public ImmutableYamlDelegate(Properties properties) { + this.properties = properties; + } + + @Override + public String get(String key) { + return properties.getProperty(key); + } + + @Override + public String get(String key, String defaultValue) { + return properties.getProperty(key, defaultValue); + } + + @Override + public Set getPropertyKeys() { + return properties.stringPropertyNames(); + } + + private void printProperties() { + properties.forEach((k, v) -> log.debug("Get property {} -> {}", k, v)); + } +} diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java index ebf668a3124b..cc07accc9b8b 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java @@ -35,6 +35,8 @@ private Constants() { */ public static final String COMMON_PROPERTIES_PATH = "/common.properties"; + public static final String REMOTE_LOGGING_YAML_PATH = "/remote-logging.yaml"; + public static final String FORMAT_SS = "%s%s"; public static final String FORMAT_S_S = "%s/%s"; public static final String FORMAT_S_S_COLON = "%s:%s"; @@ -683,9 +685,6 @@ private Constants() { public static final Integer QUERY_ALL_ON_WORKFLOW = 2; public static final Integer QUERY_ALL_ON_TASK = 3; - /** - * remote logging - */ public static final String REMOTE_LOGGING_ENABLE = "remote.logging.enable"; public static final String REMOTE_LOGGING_TARGET = "remote.logging.target"; diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/PropertyUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/PropertyUtils.java index 8289b1447903..82d4de9599bc 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/PropertyUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/PropertyUtils.java @@ -18,9 +18,11 @@ package org.apache.dolphinscheduler.common.utils; import static org.apache.dolphinscheduler.common.constants.Constants.COMMON_PROPERTIES_PATH; +import static org.apache.dolphinscheduler.common.constants.Constants.REMOTE_LOGGING_YAML_PATH; -import org.apache.dolphinscheduler.common.config.IPropertyDelegate; import org.apache.dolphinscheduler.common.config.ImmutablePriorityPropertyDelegate; +import org.apache.dolphinscheduler.common.config.ImmutablePropertyDelegate; +import org.apache.dolphinscheduler.common.config.ImmutableYamlDelegate; import java.util.HashMap; import java.util.Map; @@ -37,8 +39,10 @@ public class PropertyUtils { // todo: add another implementation for zookeeper/etcd/consul/xx - private static final IPropertyDelegate propertyDelegate = - new ImmutablePriorityPropertyDelegate(COMMON_PROPERTIES_PATH); + private final ImmutablePriorityPropertyDelegate propertyDelegate = + new ImmutablePriorityPropertyDelegate( + new ImmutablePropertyDelegate(COMMON_PROPERTIES_PATH), + new ImmutableYamlDelegate(REMOTE_LOGGING_YAML_PATH)); public static String getString(String key) { return propertyDelegate.get(key.trim()); diff --git a/dolphinscheduler-common/src/main/resources/remote-logging.yaml b/dolphinscheduler-common/src/main/resources/remote-logging.yaml new file mode 100644 index 000000000000..2cb48750a470 --- /dev/null +++ b/dolphinscheduler-common/src/main/resources/remote-logging.yaml @@ -0,0 +1,61 @@ +# +# 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. +# + +remote-logging: + # Whether to enable remote logging + enable: false + # if remote-logging.enable = true, set the target of remote logging + target: OSS + # if remote-logging.enable = true, set the log base directory + base.dir: logs + # if remote-logging.enable = true, set the number of threads to send logs to remote storage + thread.pool.size: 10 + # required if you set remote-logging.target=OSS + oss: + # oss access key id, required if you set remote-logging.target=OSS + access.key.id: + # oss access key secret, required if you set remote-logging.target=OSS + access.key.secret: + # oss bucket name, required if you set remote-logging.target=OSS + bucket.name: + # oss endpoint, required if you set remote-logging.target=OSS + endpoint: + # required if you set remote-logging.target=S3 + s3: + # s3 access key id, required if you set remote-logging.target=S3 + access.key.id: + # s3 access key secret, required if you set remote-logging.target=S3 + access.key.secret: + # s3 bucket name, required if you set remote-logging.target=S3 + bucket.name: + # s3 endpoint, required if you set remote-logging.target=S3 + endpoint: + # s3 region, required if you set remote-logging.target=S3 + region: + google.cloud.storage: + # the location of the google cloud credential, required if you set remote-logging.target=GCS + credential: /path/to/credential + # gcs bucket name, required if you set remote-logging.target=GCS + bucket.name: + abs: + # abs account name, required if you set resource.storage.type=ABS + account.name: + # abs account key, required if you set resource.storage.type=ABS + account.key: + # abs container name, required if you set resource.storage.type=ABS + container.name: + diff --git a/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/config/ImmutablePriorityPropertyDelegateTest.java b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/config/ImmutablePriorityPropertyDelegateTest.java index efba923a5a5b..633325049274 100644 --- a/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/config/ImmutablePriorityPropertyDelegateTest.java +++ b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/config/ImmutablePriorityPropertyDelegateTest.java @@ -19,6 +19,7 @@ import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable; import static org.apache.dolphinscheduler.common.constants.Constants.COMMON_PROPERTIES_PATH; +import static org.apache.dolphinscheduler.common.constants.Constants.REMOTE_LOGGING_YAML_PATH; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -26,7 +27,9 @@ class ImmutablePriorityPropertyDelegateTest { private final ImmutablePriorityPropertyDelegate immutablePriorityPropertyDelegate = - new ImmutablePriorityPropertyDelegate(COMMON_PROPERTIES_PATH); + new ImmutablePriorityPropertyDelegate( + new ImmutablePropertyDelegate(COMMON_PROPERTIES_PATH), + new ImmutableYamlDelegate(REMOTE_LOGGING_YAML_PATH)); @Test void getOverrideFromEnv() throws Exception { diff --git a/dolphinscheduler-common/src/test/resources/remote-logging.yaml b/dolphinscheduler-common/src/test/resources/remote-logging.yaml new file mode 100644 index 000000000000..cb149a77fe0d --- /dev/null +++ b/dolphinscheduler-common/src/test/resources/remote-logging.yaml @@ -0,0 +1,61 @@ +# +# 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. +# + +remote-logging: + # Whether to enable remote logging + enable: false + # if remote-logging.enable = true, set the target of remote logging + target: OSS + # if remote-logging.enable = true, set the log base directory + base.dir: logs + # if remote-logging.enable = true, set the number of threads to send logs to remote storage + thread.pool.size: 10 + # required if you set remote-logging.target=OSS + oss: + # oss access key id, required if you set remote-logging.target=OSS + access.key.id: + # oss access key secret, required if you set remote-logging.target=OSS + access.key.secret: + # oss bucket name, required if you set remote-logging.target=OSS + bucket.name: + # oss endpoint, required if you set remote-logging.target=OSS + endpoint: + # required if you set remote-logging.target=S3 + s3: + # s3 access key id, required if you set remote-logging.target=S3 + access.key.id: + # s3 access key secret, required if you set remote-logging.target=S3 + access.key.secret: + # s3 bucket name, required if you set remote-logging.target=S3 + bucket.name: + # s3 endpoint, required if you set remote-logging.target=S3 + endpoint: + # s3 region, required if you set remote-logging.target=S3 + region: + google.cloud.storage: + # the location of the google cloud credential, required if you set remote-logging.target=GCS + credential: /path/to/credential + # gcs bucket name, required if you set remote-logging.target=GCS + bucket.name: + abs: + # abs account name, required if you set resource.storage.type=ABS + account.name: + # abs account key, required if you set resource.storage.type=ABS + account.key: + # abs container name, required if you set resource.storage.type=ABS + container.name: + diff --git a/dolphinscheduler-master/src/main/assembly/dolphinscheduler-master-server.xml b/dolphinscheduler-master/src/main/assembly/dolphinscheduler-master-server.xml index 9fc3a3b67967..d521e53bc2b3 100644 --- a/dolphinscheduler-master/src/main/assembly/dolphinscheduler-master-server.xml +++ b/dolphinscheduler-master/src/main/assembly/dolphinscheduler-master-server.xml @@ -52,6 +52,7 @@ ${basedir}/../dolphinscheduler-common/src/main/resources **/*.properties + **/*.yaml conf diff --git a/dolphinscheduler-worker/src/main/assembly/dolphinscheduler-worker-server.xml b/dolphinscheduler-worker/src/main/assembly/dolphinscheduler-worker-server.xml index 70622942f0d5..5ac9b6350d11 100644 --- a/dolphinscheduler-worker/src/main/assembly/dolphinscheduler-worker-server.xml +++ b/dolphinscheduler-worker/src/main/assembly/dolphinscheduler-worker-server.xml @@ -53,6 +53,7 @@ ${basedir}/../dolphinscheduler-common/src/main/resources **/*.properties + **/*.yaml conf From 1e2358e67ae6a0a3b94f92cb8a15fd331bcd14b8 Mon Sep 17 00:00:00 2001 From: ikiler Date: Tue, 7 May 2024 16:04:50 +0800 Subject: [PATCH 061/165] Supplement the Dinky documentation --- docs/docs/en/guide/task/dinky.md | 11 ++++++----- docs/docs/zh/guide/task/dinky.md | 12 +++++++----- docs/img/tasks/demo/dinky_task_id.png | Bin 221254 -> 365529 bytes 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/docs/en/guide/task/dinky.md b/docs/docs/en/guide/task/dinky.md index c229c42ded2d..3c11594060b5 100644 --- a/docs/docs/en/guide/task/dinky.md +++ b/docs/docs/en/guide/task/dinky.md @@ -17,11 +17,12 @@ it will call `Dinky API` to trigger dinky task. Click [here](http://www.dlink.to - Please refer to [DolphinScheduler Task Parameters Appendix](appendix.md) `Default Task Parameters` section for default parameters. -| **Parameter** | **Description** | -|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Dinky Address | The url for a dinky server. | -| Dinky Task ID | The unique task id for a dinky task. | -| Online Task | Specify whether the current dinky job is online. If yes, the submitted job can only be submitted successfully when it is published and there is no corresponding Flink job instance running. | +| **Parameter** | **Description** | +|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Dinky Address | The URL for the Dinky service, e.g., http://localhost:8888. | +| Dinky Task ID | The unique task id for a dinky task. | +| Online Task | Specify whether the current dinky job is online. If yes, the submitted job can only be submitted successfully when it is published and there is no corresponding Flink job instance running. | +| Custom Parameters | Starting from Dinky 1.0, support for passing custom parameters is available. Currently, only `IN` type inputs are supported, with no support for `OUT` type outputs. Supports the `${param}` syntax for retrieving global or local dynamic parameters. | ## Task Example diff --git a/docs/docs/zh/guide/task/dinky.md b/docs/docs/zh/guide/task/dinky.md index cab9a24ce40c..c5c23f51304b 100644 --- a/docs/docs/zh/guide/task/dinky.md +++ b/docs/docs/zh/guide/task/dinky.md @@ -17,11 +17,13 @@ - 默认参数说明请参考[DolphinScheduler任务参数附录](appendix.md)`默认任务参数`一栏。 -| **任务参数** | **描述** | -|-------------|---------------------------------------------------------------------| -| Dinky 地址 | Dinky 服务的 url。 | -| Dinky 任务 ID | Dinky 作业对应的唯一ID。 | -| 上线作业 | 指定当前 Dinky 作业是否上线,如果是,则该被提交的作业只能处于已发布且当前无对应的 Flink Job 实例在运行才可提交成功。 | +| **任务参数** | **描述** | +|----------|----------------------------------------------------------------------------| +| Dinky 地址 | Dinky 服务的 url,例如:`http://localhost:8888`。 | +| Dinky 任务 ID | Dinky 作业对应的唯一ID。 | +| 上线作业 | 指定当前 Dinky 作业是否上线,如果是,则该被提交的作业只能处于已发布且当前无对应的 Flink Job 实例在运行才可提交成功。 | +| 自定义参数 | 从Dinky 1.0开始支持传递自定义参数,目前仅支持`IN`类型输入,不支持`OUT`类型输出。支持`${param}`方式获取全局或局部动态参数 | + ## Task Example diff --git a/docs/img/tasks/demo/dinky_task_id.png b/docs/img/tasks/demo/dinky_task_id.png index f1e791caad673eccdac7eb5e5b02512569d6ee35..b1ebb3ec03777bd45f1ca07ec2f9197c51bc5bb2 100644 GIT binary patch literal 365529 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�?!P*K6u?Qz>O&lVp)XHB*{UMQw1qp6HVl`ho)vd#^3=i=w%{Q5WXP3Pbp-NErpNqG?) zW==BvE9wD$u0*fo;Exq|2$3BWziPNrCGW)Uc6KG=E#4XsQ+$ylD#en`@roUk*prVp1Ugz2Yj3;2aPyTz4=OBE^RLMSWARuj8BSgs5rf^m0XtZRHe zt0oYH&7`A#GeSL_iH|?YkW#%|gCx$QlV;fNK4R>a!?CZzf|l6^)H%GI(lGv7BWlht z4DKqW>vjgVRJPY2z1|NaZlD=8cIypBF$ zRyQVx$&3r^yPG{uvtSloh)D8{f4ECec}%9#X?a-A=ggs|JlPLwCt*PB(Xz_r+i~kXq&b}eIG-}x~t#Up(I0{65`&roeN)?09A(`Xr z^O~qtKhgz9pdU%|5thU8(eXG6cfUjT-sUXI^R$f<+&(Nv_(Vl`8rmqFt;2#09haUy zIhAc5+|}Huxf4s}Uw%~jogdSjRvri6=H(<63|kp#wBne-6{r}~Zedy7F*;7S(>=ee zIs1G=Y0SL+Eh!MX>U{dJ1iOf&`|K<*CR?)wr7wVUP2s#7-EE5aN}YlFv~oY&Z!(iR zmV;CK+R-G-FN;7p)M*OX(qir51fp7G;p8TSUy111NwZZ@>D?-2-u!OliVBaB6dEcG z$pJ6Z1b@GNb1RE6x=I4Pvm(+LqfL9=zOZ9NLKr<{@GP1(O2_HuppQ39B_e)S zZU&8+a+iBoDm#+T`_6Ei`O;#b^m*!h!@*d}TC^fHC8PiB5A;~cj0hw!1$Mi5wdHn> zXB1y_|G@ZhiPL@XZ&@VB*3}ce`}X--gOEYT?=WJ?>zuLUQ!X*YDG8Hi|4Fz)#@$xk zf$)>}V->m0&Ar2V)!paH{zW-wV)nbAhLec(sUJSNydK9epS74gCQV=tXA(#YnNPI6 zbiYrX8N5EV^S}S;lp@y4G62mHLquBv1(NMmj6r`Es%oqC_a?pUUzb}yI_b=+#NE5N z#j8>~>SvT`)wNholZ_)lgv_ahR8Nq4c)-Q9^NqX>M>-jhBf6}OgdZshhtMFRve<0c zQnj7URFdLs!AJjTXnI-wjmtiqNQUs6^N?Oh+f?(Ky@~b?WEl$BikqqIk&rVfU(tp? z7;k!M!0RF4SYLHMO&Cbb?I~!x8oWs!$rAkD(P&xnwee6qIIE6{ytO_B{GL_GE~=R- zDaKw7&|+>#TaMejipBo%gf}xMI+-n(%A0_J5%+V~VEvK1v@<}4C=E|Lh%4)1+bK$W zjX3pcT(zJBOP@^vrhk|lwCihXjO|1)JLqd?uCsUyd^6);`>OcqAWR?J(2aI zghm|ca5kX9Zsd(b+%Ii39Y$G=tfx5cW<5406ZA(@qBTG#ys)8I%JHm_ z+#WoaM=9bXzc*a?^75F@OPDO|{ixJ^uK*c1YPwceFlhHf-!&qF2)zVYy$@N$@1n=+ zowUyD>`|O3s-U5W*+c)0YpgJHWnP$r)_B~9WzNA>G?|>-L6(9lRLNIh=cri5QKjcM zwzM`L)FGJN;PVIZq)s~;`VZ-3*ZPnDzLuUS0z~1pcq(eja>IG#sh~|j4A@fU=3%mP zNXLgjD?2+IE33;w@W@=MI}AG2Mi=LXXDE>3b`6S7yUd)$H!TaY`ryEa@rbCBUh)x* z_z2F0x>2^+L;xob{tQzw5%a{T)o4Kc_*GO(UW-ZLkStyVuRov-7oze?)7-qcW+GbW z9buh^V^#8d&p><^xx2g*eOv{g0rF7N5-eVKL9>SKKVq^xKy{M%%}E(O*&bhw3gU1s zjLvu6fWsT2fw&ff04WUyZ@nQEhs3}ZKKir*C%**U-ClkE;;jle}9ARK40&=KPw7Y`lhPDx&+(?)GwXbqUj}ASIBVL z{>D+T27Zg`{Dr`hPeW#iaWuofSfj$=pQBRn8BY4V@AuTXDgY_W34qiGf^qLAnv$RN zUVhAQ+8^ev_LzEpNJv?oygPe+{yt$o?_U%!=Z54}`mV;})qNy}S~M>=_SB zG%qM}%D8+(-~TcpE!EYv1$Hv`Bh%PLM1DqP2lULve0IG$xl8T*R$JcjZYhOLhC4f- zPF5Wrr=^$#?!L1YwQGEdQPo>({0OfMk;!Z8H1odw>?hXjD2GHetlyjz{95-iNv*W5 zS>ulb!n*=?y}>wh(=qd2pI7sn+DmscC8G9%dU z65+54X|Ai&j9w#79?6}GHvC|Ja*!VdbG4KAnlKpos}W?D{oiA@lQ=83&p2S1JLi(9+{ZswyDp#nZP&f?Wd=OY;~FfWFDtRuYVi12nI5=3(QP?Sv6M;FcJisp4wfXs(<>l3x znP3ZB+kOsCP8N+cMtdaAcSfCG(H2s^k`{N3&6i_#-lQ8L&eYqzzKB9w9=pGmqs#Nr z`d-Dn845OkQ3}3b5b)V&DVE#j5cROJInCg8TC9=-34AYP$DRQrJ(dyY& z_xML}baYw`6GTP{41Q4s-Xmo!)m`l;X} z^s=ER0I!*h=`O(+`S+4mtMNz2b+OgjZ85YY9kp;i?YJ`Z#^zheFZu>7)h1(9@uf{2 z8H>ruOzTf8QWTV8JvPqOaRP5K0<6`gluTTwf2x(0QNc6`g^K2NqTkb=3)jD*U96;K zBSJl$Sx-4?S4td~*0dgW%GO_>OOj=YdxqF3oLx5kq^^9QYA5wV@&0^A)p{l|=?Tp0 z^!lE?I1dKT-~AcbowhNf)9^5|`gpCZWl?+3bG_lM zaK*Bj%E9e&z9BVOQAv5-LpVEWkiniVVY$;8eJ^>``O~i3A|jS@(poN`HLqg?!=M9* zTDz~+QX5*dNuqRMCCXjLH1UXmY&K4~x-{MBN4z9V@#71%tODQj5`nRZMv}(FBgY@N z_vGUXiQ)RGoH&x5{Eu48Ve7VR0YpzU(h{d8bs8fG%oF>7#c)|!)%RB$6K{e{qxz8- z8UE;%?3jMqq)JhlVCDlY(U2 zS9V!=1fU-BOtz8|KQS?(!>)Z`_IY&h1HCfpE}fhL@vF@^&R`YfVLp{T?}(SAQFq)d zi2lIW;M;Ze-J>QQ@x4DJVjX&Pcp_|_0vfDbp5-pb5dx6uLaX%Kb~7p}>ekj4DwWpf zNG>apda-?ecIvbZ2m~MsH9vRG1fCim-oxtmuYzKnaFv&cEAAX$e+G)xq1T=@;dgWaCb%XZrnZT|LB_|QLxMGO1i;rc4hfL}0InNA$&j}C5 z3F@~uIW?e%BfB^OgpiN@?`2hFTK=p{+&jWg<$P>ViAk#&_h!iKk>?uwlh1WNBh4j; zOmvKrn0pk?&GpdDq#k*0r)2szYtY6#u5r}Gm;`+8PH)MF`&fqAFl6CEMYKWu%i&6Tj@73te)*@}ak^NQj3t$gU-musgM{<{XqDt)ydGn} zPlxqCS9K``{s@AS89JL(O!f&G_oq5@aapwVDOo$ADx zN%~g6$=>HI$lA^e4L(ahGYP>1FQj>{*ZBMWy9`9fjXRXiR*{~EHu&}K5=1XzEUigk z^E<`6y_81Gvvaq}eELhjH*b(3kqZG569=+=Vme|rGlQzkG9NdgJ4oFL^qvxH+}@<) z!+s{`;x8l!gC{d1QYBc}kz<+4J0E3lNF_7>K(XlPJI73A&2ixE!aO>rd2vu%t1C2R zYhk_YZyymL{+1tUQApeoYn(4r!^O%i6U;N^uPDUrhO}N)by(t+J@G}OI&OlYHLUCm z9}(z%FgxvOmY;AQ$sE^u>-@uKn5a}kHN;#q6nSWGY!DY~gWXOf9iTY{_%2NS@X*xc z+|r_(${Fd*%tYn|Glim5TK}50^xeHye-dNt3?R+XrRjws%?W`zU)vI|o91jy8kca0!w|si6!-f=(n%jQ0Yp6Z(7NOYnH3k)hM}CQzgo zhd*B;0wn|wAIUD$&{>I>Dj>nrh1=aPw@*&2@8*}5v`{`wd{E>7X5fLklpUPZt6oj# zo?#c|{HnoSEB$8A3C_fb#Zg8^ zU{CMt`^Njqd-{g;+o_%3!^XQF2EN8*V2+C?2DZl2R@m8|nK5))w@Q6ES6oIbqeAFK zO;B~0_hDl4s)5hm@PSkKhPE=+)yNR9)dI?FO+I2sD1rBFBED2CN^wQO!<9xPQpZ#L z%dLi+k{|idN~C7ApdQV+`l^)Qz|M)^wF{+!tNdr;Zw3#Zm*J&TA>HhHPG5gqR8Pqq zTG=tzqK=KtcD)_{DrY|Ty1qR;?MA__tGKy1$R_kS8u)dPbqg-p>@{exUE=Zce--!1 zY$s^G@q8SJb6cl!sMafBc@y3!Bkv3ED>*Lx=BewTEfwgR3Q34f*pCbPE+kUjj=#4z z=yQ_`P9fVnF594{3K~AxA+47bRHn;VyC|8j+*bGxL=Nwqq@`a7^u1r+=QDTuubg}S znV6X!;V5d)O!Vr$$unLqmlArHH&6?as&a7?zMtUkU$CWiecVbm80B>g#s$$L#ZVUs z`EAZ=9Ivi@?Gs09o|y{=OCH}2343E(#UAzNuEt z7G}H8-hK8Ty{b7+R->d=3FULauUrRNG-3}`08)}A4#Q5_5nx8NweDJ&tk11>?!0yJx=Y^%F@$v0K za2=)NF2MDl-PfT=`8eM~WcI+@(zE*M@g((aKUc$`hZxg1XUA_I+zSw`DC_8O zN6pIJfLA4UZ<#JRwf&|as)YMe+kcR=5jnNcJ~rG;$njCCn_yJ{98C8oGp}!E{c8C5 z@c#u}a==c`oF%5*P(Tkn14&v?>huemFSIYC{$TZX;#~kU<~PAMC-Y(QE)!#xC(ze; zc?L!WLR=Xp3I3H!Oy%hT4#&im`J&GVzu%td+nj4DNl47E0XCW`bd;Z(X*-fE@KmoX zH@qKPU=e_EnZU2=L_llG|Kz!MRw7PaHZCr=QMl}yY4D2T966~WdaA0k2j&_Y?80e` z@VX{lkeQj8f>!ek`<(o$qzSm02?;^`vb%c_3W`g)kT#RfXZKFLOdq9?eeQB1+NM0& z41BS}BZaFVx$APUQH7?Q`OtokgMi`P;~DIC>gA`&fkDoq&c}RD84@ZK`m}y?1Vsw7 zRzD=a=nAI|2d4OR(rrJX?o_X&gkjGr7$?cZ!|v0?ClOL8NHKHLk5qr+ED~)S4r~iU z7-JLLWW6-w2v;q7B+yhh7W7~tQMi>`aiZmsUbIeTwO}KPu3TG;pR9=l1kO)8qtm4Y z;n=mt=nf?`yK6QnyA#CYw*?6thMb;u;kwS?-r@P}emCH}fsM6>+HH;&95fK!PK;+f zr3h)kLLLJE8Mq5KSXnM#NPvTcJ;UMOGTqj!EA6$%;W8 zcc9mNs%q0F=Vh3-RQIwq4jpdWqC)Z=ZOn-m5f}^V1lFn_Ro_>xof%JSr?I=W<@0QH zWY|!4{m8beiR0~%7J0TY2z=NJo|}@-DDTP&rGi(oeb{$}Fa=*@i;h6`Q$d%&I`ZM? zoOn?V(_gk}nA%c)HkJ&e4zq)6!*>+CF-2FCTvr~mpq^(zcc)!e5WZYp_2Tpq1-tnt z73Uj`hQZVwud%AG*Pp<5!J?JrIhcg|dA!8C!;F-)!9qYUl$@aQhCs4Ea5g;;^Eu4)%Q306DpgkPM1&#ac zhl%m?^Y1XVd?olC?WNb{ooe+dtpP6<#@Rp)9x@!mb&q>aZ4;B+EcuNa9{l=@&?Jp%=>W5I* zEN(?C{Z37v;n@#3S?VR_RtA`xe*+qh?zhgH8E4pcEhkRqN=m-JKUKd&-Q)hXytT-t zXnhnx2I2(obOv#>+(Qz_M)4rLE~ADRmb2yb96YiBqzf}$#|j$uEDqLk`p@rlqgNeB z+yfE`7YAj}6Xr_bA8{wM&Z5Fq3Mm#Of@crXfOTxMN3k=%6R{`pgAn?I?n!+LSjRH3yQCW8jyEBZr!W3_kz0Q+Fq5R556TlxW{5H$ z+SwiS<~alv$|ZYxd6DaDa=`VQhQV;WqfKON%^~O4Tdeb#uoItr%(mGlTqw`R=GDCU zEmbrY=vbEcKhU05*tlW>^apAU(l5DWJKDk#aX=*e z(}tA-ARO6kLz!>U8H3S~(IuQ6zOSA}M~1s#rR&{Yio4`TM~>u;AY4$)tEy!|QPH-T z?!Y?0+`L+=KD5)!Xj(3rY0pwLL2IYipXiWHaTya0PbTPStZ$hYVfk&xF4MtlBAR^t z)2m$YY3uZd&iRAz>t>SpW8y+^;4qw2DfzX8G+;(kCA486oN9wp2{S0EzaWSlag^ z1Rwin>kUQVy8!=8Whi~|{(*gH3A7Of*dySx6u%D2|HxGTD|DZmtKRjgJE|hEqT-oi zC2KuJE%rmup+&r4no2Ci8+%PV*%llGF-wl9D;a);07z5jKPG}P7tuj?wpPIBkklD7 zE(s|VpiLZ2&3nQBjtwd3b!R;pq$neUiUYD*sM_P7Y@zIi1kQ{yAs&kM)UP~gXc;+b zomdGvGX^?@3XyCCVR3D8|1qGM^SzMP%)nyhrnj=H6t~1LM03U|fn5_88u%^esjgzR zy9`p3izNq+_v2?!j;T+7n5R?avt!el9~|e9w=$KA@aq`oIEx)%g%x{JCDr2>s_`bU zS=9Sy0Zn*w@p=tN2E1jM#hTaYt?ol@LB3rVv$vN`V&9hClZ@64%Io56#gaKVX6rRi zm-S@Gk?`fYa_9!W2G>)p)N!++I|FZ@4u|`=I1=Iw^3R3%`e2#7SdHp8ROW&rs>pe^ zaGa0O+U0Yfu|A9e3f0Ren1ZOv2Kz^$m#F@89q}?C1*7m1DqZaD!K0~zYJL_!ULT}K z$L>3d)WagMA*V5dgPlFJZVdJX+_>{TfhE0y2S{S>182^N4m?x*QcRN_d1HB~5dr#O zDl&)1r}QXfKiQznEJi@yO+|qfY^;5xHU37EzHtT7P#~lYYGgLkEZzXjjlU*=6G_1W z8x8uZV(k+T)L(nB2;e+PiPtxe3+>}AMg>D=dKL*$NfM(6_4cW`!G9I&g>J;#3izmt|Cem)d+m>s&BNKga`A z!JK@by0`JGSFJ^~J3u5s#nnzk4qyKyP|eUq%cSaZMVcc)zrA6NYHlHL#BAbcg;Y&+ zA5UGh=AtFi8&Sft*H6@?ytgE#ZdqM2)11m3_QesoW>Y78Lr=1TAqoUQQ4@Ij&lQgTXE%tK>>{47ud8Q{(4n*_M-& z09HZI&Fu*ZahuBI4@&ldR^lSZNJ>Xn(|4_Fp=qciYbYV9ZrpQxV`OYqGA10l<(#U4 zgy9%gyF~=><)s>l);>SY9G6deDS!NIeG;?~v$Gv?Xl( zND1!EkvdmWJ1Djxdd3!4|8^pZPV%|MFT7>xm*xUJ7uVCKXrcUmozFA#$!QtlpLA61 zQEUG~hdofEWPkT0i8L<_L*^Ke?+KM+^gMuh*BNSg<;3|bAWj)98 z-+983>t~%b?(c-5U}Y7erzhv&SloE$iS)mNzp#^{VS5J1YgvdWD{Gp4u~*)jxVyV{ zvsLA9tUmX*;&9vJ%Edv4#(Ob~(KXY_V$4 zzpzM8Zpbq`Wz%1>`JMK1-i0-q*A*3RR;tC$6JSPNvntFi69gXffNLHQG7_TSl<@xT zBsv;^n4h;YVMLe9?(OlyXv!brJ*5R`w~^oTV{2EEmTCU>WT~*Li|9xr+1=|LNqA12 zDJ*ygOt0SXpBx^J8!??(Gch*y_Vx}gB;i*#mzK8kXsvN(sp}b)labMCveuH43i~C( z?|DOWjTUzGz#%Z4#Ha)QUAq7JR9m)Gppg{tclql@5sru`7$4oH!v{kdYDD{UB``?xpz0U0e4 zpLGsM2A6MTtA7~KuQ_6%wr9UEHa=MV$mmYhpKn~yY0ZAVN1IeYGr>;#AgRm$1lJ_? zB7>g%&mbvf+{H)c8`0TvXrQfMg55H%$M@1@n~+}KvJs$#!{S#n|n=}+%_MX
{Jq$TG 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

^l2<%RLN}{~AYmU&ftLR%}W<@(6IFGO2&BEDc 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

9#9;SB%kL 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�>Y-Sd@~Al= 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 jTHPr>?s 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%0D#V-H^!Cc+Y1qw&9UV}B-w#0A?dZc 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 Mn|i7RH|!t$FSU&d)Bpeg From 99d8276be7ff1f0cb47f6d67d7440a9380614073 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Wed, 8 May 2024 11:46:09 +0800 Subject: [PATCH 062/165] Optimizing the scope of RPC base classes (#15946) * Optimizing the scope of RPC base classes * Fix UT --- .../alert/rpc/AlertRpcServer.java | 16 +-- .../alert/rpc/AlertRpcServerTest.java | 28 ++--- .../api/service/LoggerServiceTest.java | 19 ++- ....java => AbstractClientMethodInvoker.java} | 5 +- .../base/client/ClientInvocationHandler.java | 5 +- .../base/client/ClientMethodInvoker.java | 2 +- .../base/client/IRpcClientProxyFactory.java | 2 +- .../JdkDynamicRpcClientProxyFactory.java | 5 +- .../base/{ => client}/NettyClientHandler.java | 18 +-- .../{ => client}/NettyRemotingClient.java | 110 ++---------------- .../NettyRemotingClientFactory.java | 2 +- ...gletonJdkDynamicRpcClientProxyFactory.java | 1 - .../base/client/SyncClientMethodInvoker.java | 5 +- .../extract/base/future/ResponseFuture.java | 76 +----------- .../base/server/JdkDynamicServerHandler.java | 12 +- .../{ => server}/NettyRemotingServer.java | 57 +++++---- .../NettyRemotingServerFactory.java | 6 +- .../extract/base/server/RpcServer.java | 74 ++++++++++++ .../base/server/ServerMethodInvoker.java | 4 +- .../base/server/ServerMethodInvokerImpl.java | 9 +- .../server/ServerMethodInvokerRegistry.java | 28 +++++ .../SpringServerMethodInvokerDiscovery.java | 37 ++---- ...onJdkDynamicRpcClientProxyFactoryTest.java | 12 +- .../server/master/rpc/MasterRpcServer.java | 18 +-- .../master/rpc/MasterRpcServerTest.java | 38 ++++++ .../microbench/rpc/RpcBenchMarkTest.java | 14 +-- .../server/worker/rpc/WorkerRpcServer.java | 18 +-- .../worker/rpc/WorkerRpcServerTest.java | 39 +++++++ 28 files changed, 301 insertions(+), 359 deletions(-) rename dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/processor/TaskResponseProcessorTestConfig.java => dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/rpc/AlertRpcServerTest.java (61%) rename dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/{BaseRemoteMethodInvoker.java => AbstractClientMethodInvoker.java} (83%) rename dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/{ => client}/NettyClientHandler.java (87%) rename dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/{ => client}/NettyRemotingClient.java (62%) rename dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/{ => client}/NettyRemotingClientFactory.java (95%) rename dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/{ => server}/NettyRemotingServer.java (75%) rename dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/{ => server}/NettyRemotingServerFactory.java (84%) create mode 100644 dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/RpcServer.java create mode 100644 dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvokerRegistry.java create mode 100644 dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/rpc/MasterRpcServerTest.java create mode 100644 dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/rpc/WorkerRpcServerTest.java diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/rpc/AlertRpcServer.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/rpc/AlertRpcServer.java index 3bd368573a79..d73e4755ddd4 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/rpc/AlertRpcServer.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/rpc/AlertRpcServer.java @@ -18,7 +18,6 @@ package org.apache.dolphinscheduler.alert.rpc; import org.apache.dolphinscheduler.alert.config.AlertConfig; -import org.apache.dolphinscheduler.extract.base.NettyRemotingServerFactory; import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; import org.apache.dolphinscheduler.extract.base.server.SpringServerMethodInvokerDiscovery; @@ -31,20 +30,7 @@ public class AlertRpcServer extends SpringServerMethodInvokerDiscovery implements AutoCloseable { public AlertRpcServer(AlertConfig alertConfig) { - super(NettyRemotingServerFactory.buildNettyRemotingServer( - NettyServerConfig.builder().serverName("AlertRpcServer").listenPort(alertConfig.getPort()).build())); + super(NettyServerConfig.builder().serverName("AlertRpcServer").listenPort(alertConfig.getPort()).build()); } - public void start() { - log.info("Starting AlertRpcServer..."); - nettyRemotingServer.start(); - log.info("Started AlertRpcServer..."); - } - - @Override - public void close() { - log.info("Closing AlertRpcServer..."); - nettyRemotingServer.close(); - log.info("Closed AlertRpcServer..."); - } } diff --git a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/processor/TaskResponseProcessorTestConfig.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/rpc/AlertRpcServerTest.java similarity index 61% rename from dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/processor/TaskResponseProcessorTestConfig.java rename to dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/rpc/AlertRpcServerTest.java index df88de34e30a..75f16848fdf0 100644 --- a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/processor/TaskResponseProcessorTestConfig.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/rpc/AlertRpcServerTest.java @@ -15,22 +15,24 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.server.master.processor; +package org.apache.dolphinscheduler.alert.rpc; -import org.apache.dolphinscheduler.server.master.utils.DataQualityResultOperator; +import org.apache.dolphinscheduler.alert.config.AlertConfig; -import org.mockito.Mockito; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.junit.jupiter.api.Test; -/** - * dependency config - */ -@Configuration -public class TaskResponseProcessorTestConfig { +class AlertRpcServerTest { + + private final AlertRpcServer alertRpcServer = new AlertRpcServer(new AlertConfig()); - @Bean - public DataQualityResultOperator dataQualityResultOperator() { - return Mockito.mock(DataQualityResultOperator.class); + @Test + void testStart() { + alertRpcServer.start(); } + + @Test + void testClose() { + alertRpcServer.close(); + } + } diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/LoggerServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/LoggerServiceTest.java index 4861e1004e4b..972092602fe6 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/LoggerServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/LoggerServiceTest.java @@ -40,7 +40,6 @@ import org.apache.dolphinscheduler.dao.mapper.ProjectMapper; import org.apache.dolphinscheduler.dao.mapper.TaskDefinitionMapper; import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao; -import org.apache.dolphinscheduler.extract.base.NettyRemotingServer; import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; import org.apache.dolphinscheduler.extract.base.server.SpringServerMethodInvokerDiscovery; import org.apache.dolphinscheduler.extract.common.ILogService; @@ -91,7 +90,7 @@ public class LoggerServiceTest { @Mock private TaskDefinitionMapper taskDefinitionMapper; - private NettyRemotingServer nettyRemotingServer; + private SpringServerMethodInvokerDiscovery springServerMethodInvokerDiscovery; private int nettyServerPort = 18080; @@ -103,11 +102,10 @@ public void setUp() { return; } - nettyRemotingServer = new NettyRemotingServer(NettyServerConfig.builder().listenPort(nettyServerPort).build()); - nettyRemotingServer.start(); - SpringServerMethodInvokerDiscovery springServerMethodInvokerDiscovery = - new SpringServerMethodInvokerDiscovery(nettyRemotingServer); - springServerMethodInvokerDiscovery.postProcessAfterInitialization(new ILogService() { + springServerMethodInvokerDiscovery = new SpringServerMethodInvokerDiscovery( + NettyServerConfig.builder().serverName("TestLogServer").listenPort(nettyServerPort).build()); + springServerMethodInvokerDiscovery.start(); + springServerMethodInvokerDiscovery.registerServerMethodInvokerProvider(new ILogService() { @Override public TaskInstanceLogFileDownloadResponse getTaskInstanceWholeLogFileBytes(TaskInstanceLogFileDownloadRequest taskInstanceLogFileDownloadRequest) { @@ -142,13 +140,14 @@ public GetAppIdResponse getAppId(GetAppIdRequest getAppIdRequest) { public void removeTaskInstanceLog(String taskInstanceLogAbsolutePath) { } - }, "iLogServiceImpl"); + }); + springServerMethodInvokerDiscovery.start(); } @AfterEach public void tearDown() { - if (nettyRemotingServer != null) { - nettyRemotingServer.close(); + if (springServerMethodInvokerDiscovery != null) { + springServerMethodInvokerDiscovery.close(); } } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/BaseRemoteMethodInvoker.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/AbstractClientMethodInvoker.java similarity index 83% rename from dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/BaseRemoteMethodInvoker.java rename to dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/AbstractClientMethodInvoker.java index 519dd87199f1..b753f1efa773 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/BaseRemoteMethodInvoker.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/AbstractClientMethodInvoker.java @@ -17,12 +17,11 @@ package org.apache.dolphinscheduler.extract.base.client; -import org.apache.dolphinscheduler.extract.base.NettyRemotingClient; import org.apache.dolphinscheduler.extract.base.utils.Host; import java.lang.reflect.Method; -public abstract class BaseRemoteMethodInvoker implements ClientMethodInvoker { +abstract class AbstractClientMethodInvoker implements ClientMethodInvoker { protected final String methodIdentifier; @@ -32,7 +31,7 @@ public abstract class BaseRemoteMethodInvoker implements ClientMethodInvoker { protected final Host serverHost; - public BaseRemoteMethodInvoker(Host serverHost, Method localMethod, NettyRemotingClient nettyRemotingClient) { + AbstractClientMethodInvoker(Host serverHost, Method localMethod, NettyRemotingClient nettyRemotingClient) { this.serverHost = serverHost; this.localMethod = localMethod; this.nettyRemotingClient = nettyRemotingClient; diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/ClientInvocationHandler.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/ClientInvocationHandler.java index d5c9ab73d3d4..41ec3e056d19 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/ClientInvocationHandler.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/ClientInvocationHandler.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull; -import org.apache.dolphinscheduler.extract.base.NettyRemotingClient; import org.apache.dolphinscheduler.extract.base.RpcMethod; import org.apache.dolphinscheduler.extract.base.utils.Host; @@ -31,7 +30,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class ClientInvocationHandler implements InvocationHandler { +class ClientInvocationHandler implements InvocationHandler { private final NettyRemotingClient nettyRemotingClient; @@ -39,7 +38,7 @@ public class ClientInvocationHandler implements InvocationHandler { private final Host serverHost; - public ClientInvocationHandler(Host serverHost, NettyRemotingClient nettyRemotingClient) { + ClientInvocationHandler(Host serverHost, NettyRemotingClient nettyRemotingClient) { this.serverHost = checkNotNull(serverHost); this.nettyRemotingClient = checkNotNull(nettyRemotingClient); this.methodInvokerMap = new ConcurrentHashMap<>(); diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/ClientMethodInvoker.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/ClientMethodInvoker.java index dcf53b0311d8..a287fd95ce97 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/ClientMethodInvoker.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/ClientMethodInvoker.java @@ -19,7 +19,7 @@ import java.lang.reflect.Method; -public interface ClientMethodInvoker { +interface ClientMethodInvoker { Object invoke(Object proxy, Method method, Object[] args) throws Throwable; diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/IRpcClientProxyFactory.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/IRpcClientProxyFactory.java index e60b0f18b0ad..afd3adf34888 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/IRpcClientProxyFactory.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/IRpcClientProxyFactory.java @@ -17,7 +17,7 @@ package org.apache.dolphinscheduler.extract.base.client; -public interface IRpcClientProxyFactory { +interface IRpcClientProxyFactory { /** * Create the client proxy. diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/JdkDynamicRpcClientProxyFactory.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/JdkDynamicRpcClientProxyFactory.java index 5635a88f344a..bf329ab3fc96 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/JdkDynamicRpcClientProxyFactory.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/JdkDynamicRpcClientProxyFactory.java @@ -17,7 +17,6 @@ package org.apache.dolphinscheduler.extract.base.client; -import org.apache.dolphinscheduler.extract.base.NettyRemotingClient; import org.apache.dolphinscheduler.extract.base.utils.Host; import java.lang.reflect.Proxy; @@ -34,7 +33,7 @@ /** * This class is used to create a proxy client which will transform local method invocation to remove invocation. */ -public class JdkDynamicRpcClientProxyFactory implements IRpcClientProxyFactory { +class JdkDynamicRpcClientProxyFactory implements IRpcClientProxyFactory { private final NettyRemotingClient nettyRemotingClient; @@ -49,7 +48,7 @@ public Map load(String key) { } }); - public JdkDynamicRpcClientProxyFactory(NettyRemotingClient nettyRemotingClient) { + JdkDynamicRpcClientProxyFactory(NettyRemotingClient nettyRemotingClient) { this.nettyRemotingClient = nettyRemotingClient; } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyClientHandler.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyClientHandler.java similarity index 87% rename from dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyClientHandler.java rename to dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyClientHandler.java index b0d998af83ee..be570eb57733 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyClientHandler.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyClientHandler.java @@ -15,16 +15,15 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.extract.base; +package org.apache.dolphinscheduler.extract.base.client; +import org.apache.dolphinscheduler.extract.base.StandardRpcResponse; import org.apache.dolphinscheduler.extract.base.future.ResponseFuture; import org.apache.dolphinscheduler.extract.base.protocal.HeartBeatTransporter; import org.apache.dolphinscheduler.extract.base.protocal.Transporter; import org.apache.dolphinscheduler.extract.base.serialize.JsonSerializer; import org.apache.dolphinscheduler.extract.base.utils.ChannelUtils; -import java.util.concurrent.ExecutorService; - import lombok.extern.slf4j.Slf4j; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; @@ -38,11 +37,8 @@ public class NettyClientHandler extends ChannelInboundHandlerAdapter { private final NettyRemotingClient nettyRemotingClient; - private final ExecutorService callbackExecutor; - - public NettyClientHandler(NettyRemotingClient nettyRemotingClient, ExecutorService callbackExecutor) { + public NettyClientHandler(NettyRemotingClient nettyRemotingClient) { this.nettyRemotingClient = nettyRemotingClient; - this.callbackExecutor = callbackExecutor; } @Override @@ -64,13 +60,7 @@ private void processReceived(final Transporter transporter) { } StandardRpcResponse deserialize = JsonSerializer.deserialize(transporter.getBody(), StandardRpcResponse.class); future.setIRpcResponse(deserialize); - future.release(); - if (future.getInvokeCallback() != null) { - future.removeFuture(); - this.callbackExecutor.execute(future::executeInvokeCallback); - } else { - future.putResponse(deserialize); - } + future.putResponse(deserialize); } @Override diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingClient.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyRemotingClient.java similarity index 62% rename from dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingClient.java rename to dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyRemotingClient.java index e4682f5224be..3999f5c9f544 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingClient.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyRemotingClient.java @@ -15,33 +15,24 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.extract.base; +package org.apache.dolphinscheduler.extract.base.client; import org.apache.dolphinscheduler.common.thread.ThreadUtils; +import org.apache.dolphinscheduler.extract.base.IRpcResponse; import org.apache.dolphinscheduler.extract.base.config.NettyClientConfig; import org.apache.dolphinscheduler.extract.base.exception.RemotingException; import org.apache.dolphinscheduler.extract.base.exception.RemotingTimeoutException; -import org.apache.dolphinscheduler.extract.base.exception.RemotingTooMuchRequestException; -import org.apache.dolphinscheduler.extract.base.future.InvokeCallback; -import org.apache.dolphinscheduler.extract.base.future.ReleaseSemaphore; import org.apache.dolphinscheduler.extract.base.future.ResponseFuture; import org.apache.dolphinscheduler.extract.base.protocal.Transporter; import org.apache.dolphinscheduler.extract.base.protocal.TransporterDecoder; import org.apache.dolphinscheduler.extract.base.protocal.TransporterEncoder; -import org.apache.dolphinscheduler.extract.base.utils.CallerThreadExecutePolicy; import org.apache.dolphinscheduler.extract.base.utils.Constants; import org.apache.dolphinscheduler.extract.base.utils.Host; import org.apache.dolphinscheduler.extract.base.utils.NettyUtils; import java.net.InetSocketAddress; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -71,14 +62,8 @@ public class NettyRemotingClient implements AutoCloseable { private final NettyClientConfig clientConfig; - private final Semaphore asyncSemaphore = new Semaphore(1024, true); - - private final ExecutorService callbackExecutor; - private final NettyClientHandler clientHandler; - private final ScheduledExecutorService responseFutureExecutor; - public NettyRemotingClient(final NettyClientConfig clientConfig) { this.clientConfig = clientConfig; ThreadFactory nettyClientThreadFactory = ThreadUtils.newDaemonThreadFactory("NettyClientThread-"); @@ -87,18 +72,7 @@ public NettyRemotingClient(final NettyClientConfig clientConfig) { } else { this.workerGroup = new NioEventLoopGroup(clientConfig.getWorkerThreads(), nettyClientThreadFactory); } - this.callbackExecutor = new ThreadPoolExecutor( - Constants.CPUS, - Constants.CPUS, - 1, - TimeUnit.MINUTES, - new LinkedBlockingQueue<>(1000), - ThreadUtils.newDaemonThreadFactory("NettyClientCallbackThread-"), - new CallerThreadExecutePolicy()); - this.clientHandler = new NettyClientHandler(this, callbackExecutor); - - this.responseFutureExecutor = Executors.newSingleThreadScheduledExecutor( - ThreadUtils.newDaemonThreadFactory("NettyClientResponseFutureThread-")); + this.clientHandler = new NettyClientHandler(this); this.start(); } @@ -127,66 +101,9 @@ public void initChannel(SocketChannel ch) { .addLast(new TransporterDecoder(), clientHandler, new TransporterEncoder()); } }); - this.responseFutureExecutor.scheduleWithFixedDelay(ResponseFuture::scanFutureTable, 0, 1, TimeUnit.SECONDS); isStarted.compareAndSet(false, true); } - public void sendAsync(final Host host, - final Transporter transporter, - final long timeoutMillis, - final InvokeCallback invokeCallback) throws InterruptedException, RemotingException { - final Channel channel = getChannel(host); - if (channel == null) { - throw new RemotingException("network error"); - } - /* - * request unique identification - */ - final long opaque = transporter.getHeader().getOpaque(); - /* - * control concurrency number - */ - boolean acquired = this.asyncSemaphore.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS); - if (acquired) { - final ReleaseSemaphore releaseSemaphore = new ReleaseSemaphore(this.asyncSemaphore); - - /* - * response future - */ - final ResponseFuture responseFuture = new ResponseFuture(opaque, - timeoutMillis, - invokeCallback, - releaseSemaphore); - try { - channel.writeAndFlush(transporter).addListener(future -> { - if (future.isSuccess()) { - responseFuture.setSendOk(true); - return; - } else { - responseFuture.setSendOk(false); - } - responseFuture.setCause(future.cause()); - responseFuture.putResponse(null); - try { - responseFuture.executeInvokeCallback(); - } catch (Exception ex) { - log.error("execute callback error", ex); - } finally { - responseFuture.release(); - } - }); - } catch (Exception ex) { - responseFuture.release(); - throw new RemotingException(String.format("Send transporter to host: %s failed", host), ex); - } - } else { - String message = String.format( - "try to acquire async semaphore timeout: %d, waiting thread num: %d, total permits: %d", - timeoutMillis, asyncSemaphore.getQueueLength(), asyncSemaphore.availablePermits()); - throw new RemotingTooMuchRequestException(message); - } - } - public IRpcResponse sendSync(final Host host, final Transporter transporter, final long timeoutMillis) throws InterruptedException, RemotingException { final Channel channel = getChannel(host); @@ -194,7 +111,7 @@ public IRpcResponse sendSync(final Host host, final Transporter transporter, throw new RemotingException(String.format("connect to : %s fail", host)); } final long opaque = transporter.getHeader().getOpaque(); - final ResponseFuture responseFuture = new ResponseFuture(opaque, timeoutMillis, null, null); + final ResponseFuture responseFuture = new ResponseFuture(opaque, timeoutMillis); channel.writeAndFlush(transporter).addListener(future -> { if (future.isSuccess()) { responseFuture.setSendOk(true); @@ -220,7 +137,7 @@ public IRpcResponse sendSync(final Host host, final Transporter transporter, return iRpcResponse; } - public Channel getChannel(Host host) { + private Channel getChannel(Host host) { Channel channel = channels.get(host); if (channel != null && channel.isActive()) { return channel; @@ -235,9 +152,9 @@ public Channel getChannel(Host host) { * @param isSync sync flag * @return channel */ - public Channel createChannel(Host host, boolean isSync) { - ChannelFuture future; + private Channel createChannel(Host host, boolean isSync) { try { + ChannelFuture future; synchronized (bootstrap) { future = bootstrap.connect(new InetSocketAddress(host.getIp(), host.getPort())); } @@ -249,10 +166,11 @@ public Channel createChannel(Host host, boolean isSync) { channels.put(host, channel); return channel; } - } catch (Exception ex) { - log.warn(String.format("connect to %s error", host), ex); + throw new IllegalArgumentException("connect to host: " + host + " failed"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Connect to host: " + host + " failed", e); } - return null; } @Override @@ -263,12 +181,6 @@ public void close() { if (workerGroup != null) { this.workerGroup.shutdownGracefully(); } - if (callbackExecutor != null) { - this.callbackExecutor.shutdownNow(); - } - if (this.responseFutureExecutor != null) { - this.responseFutureExecutor.shutdownNow(); - } log.info("netty client closed"); } catch (Exception ex) { log.error("netty client close exception", ex); diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingClientFactory.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyRemotingClientFactory.java similarity index 95% rename from dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingClientFactory.java rename to dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyRemotingClientFactory.java index 7bbebfbf3d89..d14a8aa54efc 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingClientFactory.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/NettyRemotingClientFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.extract.base; +package org.apache.dolphinscheduler.extract.base.client; import org.apache.dolphinscheduler.extract.base.config.NettyClientConfig; diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/SingletonJdkDynamicRpcClientProxyFactory.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/SingletonJdkDynamicRpcClientProxyFactory.java index 28d82532bebd..44d310e70b1e 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/SingletonJdkDynamicRpcClientProxyFactory.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/SingletonJdkDynamicRpcClientProxyFactory.java @@ -17,7 +17,6 @@ package org.apache.dolphinscheduler.extract.base.client; -import org.apache.dolphinscheduler.extract.base.NettyRemotingClientFactory; import org.apache.dolphinscheduler.extract.base.config.NettyClientConfig; public class SingletonJdkDynamicRpcClientProxyFactory { diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/SyncClientMethodInvoker.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/SyncClientMethodInvoker.java index b5fdf3fb71cd..4731a22d0a72 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/SyncClientMethodInvoker.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/client/SyncClientMethodInvoker.java @@ -18,7 +18,6 @@ package org.apache.dolphinscheduler.extract.base.client; import org.apache.dolphinscheduler.extract.base.IRpcResponse; -import org.apache.dolphinscheduler.extract.base.NettyRemotingClient; import org.apache.dolphinscheduler.extract.base.RpcMethod; import org.apache.dolphinscheduler.extract.base.StandardRpcRequest; import org.apache.dolphinscheduler.extract.base.exception.MethodInvocationException; @@ -29,9 +28,9 @@ import java.lang.reflect.Method; -public class SyncClientMethodInvoker extends BaseRemoteMethodInvoker { +class SyncClientMethodInvoker extends AbstractClientMethodInvoker { - public SyncClientMethodInvoker(Host serverHost, Method localMethod, NettyRemotingClient nettyRemotingClient) { + SyncClientMethodInvoker(Host serverHost, Method localMethod, NettyRemotingClient nettyRemotingClient) { super(serverHost, localMethod, nettyRemotingClient); } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/future/ResponseFuture.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/future/ResponseFuture.java index 35405c557803..1fbbd9ed6c42 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/future/ResponseFuture.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/future/ResponseFuture.java @@ -19,8 +19,6 @@ import org.apache.dolphinscheduler.extract.base.IRpcResponse; -import java.util.Iterator; -import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -34,17 +32,13 @@ @Slf4j public class ResponseFuture { - private static final ConcurrentHashMap FUTURE_TABLE = new ConcurrentHashMap<>(256); + private static final ConcurrentHashMap FUTURE_TABLE = new ConcurrentHashMap<>(); private final long opaque; // remove the timeout private final long timeoutMillis; - private final InvokeCallback invokeCallback; - - private final ReleaseSemaphore releaseSemaphore; - private final CountDownLatch latch = new CountDownLatch(1); private final long beginTimestamp = System.currentTimeMillis(); @@ -57,14 +51,9 @@ public class ResponseFuture { private Throwable cause; - public ResponseFuture(long opaque, - long timeoutMillis, - InvokeCallback invokeCallback, - ReleaseSemaphore releaseSemaphore) { + public ResponseFuture(long opaque, long timeoutMillis) { this.opaque = opaque; this.timeoutMillis = timeoutMillis; - this.invokeCallback = invokeCallback; - this.releaseSemaphore = releaseSemaphore; FUTURE_TABLE.put(opaque, this); } @@ -90,10 +79,6 @@ public static ResponseFuture getFuture(long opaque) { return FUTURE_TABLE.get(opaque); } - public void removeFuture() { - FUTURE_TABLE.remove(opaque); - } - /** * whether timeout * @@ -104,15 +89,6 @@ public boolean isTimeout() { return diff > this.timeoutMillis; } - /** - * execute invoke callback - */ - public void executeInvokeCallback() { - if (invokeCallback != null) { - invokeCallback.operationComplete(this); - } - } - public boolean isSendOK() { return sendOk; } @@ -129,52 +105,4 @@ public Throwable getCause() { return cause; } - public long getOpaque() { - return opaque; - } - - public long getTimeoutMillis() { - return timeoutMillis; - } - - public long getBeginTimestamp() { - return beginTimestamp; - } - - public InvokeCallback getInvokeCallback() { - return invokeCallback; - } - - /** - * release - */ - public void release() { - if (this.releaseSemaphore != null) { - this.releaseSemaphore.release(); - } - } - - /** - * scan future table - */ - public static void scanFutureTable() { - Iterator> it = FUTURE_TABLE.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry next = it.next(); - ResponseFuture future = next.getValue(); - if ((future.getBeginTimestamp() + future.getTimeoutMillis() + 1000) > System.currentTimeMillis()) { - continue; - } - try { - // todo: use thread pool to execute the async callback, otherwise will block the scan thread - future.release(); - future.executeInvokeCallback(); - } catch (Exception ex) { - log.error("ScanFutureTable, execute callback error, requestId: {}", future.getOpaque(), ex); - } - it.remove(); - log.debug("Remove timeout request: {}", future); - } - } - } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/JdkDynamicServerHandler.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/JdkDynamicServerHandler.java index b4978172f124..f57ff0b609be 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/JdkDynamicServerHandler.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/JdkDynamicServerHandler.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull; -import org.apache.dolphinscheduler.extract.base.NettyRemotingServer; import org.apache.dolphinscheduler.extract.base.StandardRpcRequest; import org.apache.dolphinscheduler.extract.base.StandardRpcResponse; import org.apache.dolphinscheduler.extract.base.protocal.HeartBeatTransporter; @@ -30,6 +29,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; import lombok.extern.slf4j.Slf4j; @@ -42,14 +42,14 @@ @Slf4j @ChannelHandler.Sharable -public class JdkDynamicServerHandler extends ChannelInboundHandlerAdapter { +class JdkDynamicServerHandler extends ChannelInboundHandlerAdapter { - private final NettyRemotingServer nettyRemotingServer; + private final ExecutorService methodInvokeExecutor; private final Map methodInvokerMap; - public JdkDynamicServerHandler(NettyRemotingServer nettyRemotingServer) { - this.nettyRemotingServer = nettyRemotingServer; + JdkDynamicServerHandler(ExecutorService methodInvokeExecutor) { + this.methodInvokeExecutor = methodInvokeExecutor; this.methodInvokerMap = new ConcurrentHashMap<>(); } @@ -90,7 +90,7 @@ private void processReceived(final Channel channel, final Transporter transporte channel.writeAndFlush(response); return; } - nettyRemotingServer.getDefaultExecutor().execute(() -> { + methodInvokeExecutor.execute(() -> { StandardRpcResponse iRpcResponse; try { StandardRpcRequest standardRpcRequest = diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingServer.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/NettyRemotingServer.java similarity index 75% rename from dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingServer.java rename to dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/NettyRemotingServer.java index 365a17dd030f..9beeaced3d3c 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingServer.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/NettyRemotingServer.java @@ -15,15 +15,13 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.extract.base; +package org.apache.dolphinscheduler.extract.base.server; import org.apache.dolphinscheduler.common.thread.ThreadUtils; import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; import org.apache.dolphinscheduler.extract.base.exception.RemoteException; import org.apache.dolphinscheduler.extract.base.protocal.TransporterDecoder; import org.apache.dolphinscheduler.extract.base.protocal.TransporterEncoder; -import org.apache.dolphinscheduler.extract.base.server.JdkDynamicServerHandler; -import org.apache.dolphinscheduler.extract.base.server.ServerMethodInvoker; import org.apache.dolphinscheduler.extract.base.utils.Constants; import org.apache.dolphinscheduler.extract.base.utils.NettyUtils; @@ -32,6 +30,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; @@ -48,12 +47,15 @@ * remoting netty server */ @Slf4j -public class NettyRemotingServer { +class NettyRemotingServer { private final ServerBootstrap serverBootstrap = new ServerBootstrap(); - private final ExecutorService defaultExecutor = ThreadUtils - .newDaemonFixedThreadExecutor("NettyRemotingServerThread", Runtime.getRuntime().availableProcessors() * 2); + @Getter + private final String serverName; + + @Getter + private final ExecutorService methodInvokerExecutor; private final EventLoopGroup bossGroup; @@ -61,16 +63,20 @@ public class NettyRemotingServer { private final NettyServerConfig serverConfig; - private final JdkDynamicServerHandler serverHandler = new JdkDynamicServerHandler(this); + private final JdkDynamicServerHandler channelHandler; private final AtomicBoolean isStarted = new AtomicBoolean(false); - public NettyRemotingServer(final NettyServerConfig serverConfig) { + NettyRemotingServer(final NettyServerConfig serverConfig) { this.serverConfig = serverConfig; + this.serverName = serverConfig.getServerName(); + this.methodInvokerExecutor = ThreadUtils.newDaemonFixedThreadExecutor( + serverName + "MethodInvoker-%d", Runtime.getRuntime().availableProcessors() * 2 + 1); + this.channelHandler = new JdkDynamicServerHandler(methodInvokerExecutor); ThreadFactory bossThreadFactory = - ThreadUtils.newDaemonThreadFactory(serverConfig.getServerName() + "BossThread_%s"); + ThreadUtils.newDaemonThreadFactory(serverName + "BossThread-%d"); ThreadFactory workerThreadFactory = - ThreadUtils.newDaemonThreadFactory(serverConfig.getServerName() + "WorkerThread_%s"); + ThreadUtils.newDaemonThreadFactory(serverName + "WorkerThread-%d"); if (Epoll.isAvailable()) { this.bossGroup = new EpollEventLoopGroup(1, bossThreadFactory); this.workGroup = new EpollEventLoopGroup(serverConfig.getWorkerThread(), workerThreadFactory); @@ -80,7 +86,7 @@ public NettyRemotingServer(final NettyServerConfig serverConfig) { } } - public void start() { + void start() { if (isStarted.compareAndSet(false, true)) { this.serverBootstrap .group(this.bossGroup, this.workGroup) @@ -103,9 +109,9 @@ protected void initChannel(SocketChannel ch) { try { future = serverBootstrap.bind(serverConfig.getListenPort()).sync(); } catch (Exception e) { - log.error("{} bind fail {}, exit", serverConfig.getServerName(), e.getMessage(), e); throw new RemoteException( - String.format("%s bind %s fail", serverConfig.getServerName(), serverConfig.getListenPort())); + String.format("%s bind %s fail", serverConfig.getServerName(), serverConfig.getListenPort()), + e); } if (future.isSuccess()) { @@ -113,14 +119,9 @@ protected void initChannel(SocketChannel ch) { return; } - if (future.cause() != null) { - throw new RemoteException( - String.format("%s bind %s fail", serverConfig.getServerName(), serverConfig.getListenPort()), - future.cause()); - } else { - throw new RemoteException( - String.format("%s bind %s fail", serverConfig.getServerName(), serverConfig.getListenPort())); - } + throw new RemoteException( + String.format("%s bind %s fail", serverConfig.getServerName(), serverConfig.getListenPort()), + future.cause()); } } @@ -135,18 +136,14 @@ private void initNettyChannel(SocketChannel ch) { .addLast("decoder", new TransporterDecoder()) .addLast("server-idle-handle", new IdleStateHandler(0, 0, Constants.NETTY_SERVER_HEART_BEAT_TIME, TimeUnit.MILLISECONDS)) - .addLast("handler", serverHandler); - } - - public ExecutorService getDefaultExecutor() { - return defaultExecutor; + .addLast("handler", channelHandler); } - public void registerMethodInvoker(ServerMethodInvoker methodInvoker) { - serverHandler.registerMethodInvoker(methodInvoker); + void registerMethodInvoker(ServerMethodInvoker methodInvoker) { + channelHandler.registerMethodInvoker(methodInvoker); } - public void close() { + void close() { if (isStarted.compareAndSet(true, false)) { try { if (bossGroup != null) { @@ -155,7 +152,7 @@ public void close() { if (workGroup != null) { this.workGroup.shutdownGracefully(); } - defaultExecutor.shutdown(); + methodInvokerExecutor.shutdown(); } catch (Exception ex) { log.error("netty server close exception", ex); } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingServerFactory.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/NettyRemotingServerFactory.java similarity index 84% rename from dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingServerFactory.java rename to dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/NettyRemotingServerFactory.java index 6bf1b8d31ce2..70ed0529e803 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/NettyRemotingServerFactory.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/NettyRemotingServerFactory.java @@ -15,16 +15,16 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.extract.base; +package org.apache.dolphinscheduler.extract.base.server; import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; import lombok.experimental.UtilityClass; @UtilityClass -public class NettyRemotingServerFactory { +class NettyRemotingServerFactory { - public NettyRemotingServer buildNettyRemotingServer(NettyServerConfig nettyServerConfig) { + NettyRemotingServer buildNettyRemotingServer(NettyServerConfig nettyServerConfig) { return new NettyRemotingServer(nettyServerConfig); } } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/RpcServer.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/RpcServer.java new file mode 100644 index 000000000000..213868ba46e4 --- /dev/null +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/RpcServer.java @@ -0,0 +1,74 @@ +/* + * 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.extract.base.server; + +import org.apache.dolphinscheduler.extract.base.RpcMethod; +import org.apache.dolphinscheduler.extract.base.RpcService; +import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; + +import java.lang.reflect.Method; + +import lombok.extern.slf4j.Slf4j; + +/** + * The RpcServer based on Netty. The server will register the method invoker and provide the service to the client. + * Once the server is started, it will listen on the port and wait for the client to connect. + *

+ *          RpcServer rpcServer = new RpcServer(new NettyServerConfig());
+ *          rpcServer.registerServerMethodInvokerProvider(new ServerMethodInvokerProviderImpl());
+ *          rpcServer.start();
+ * 
+ */ +@Slf4j +public class RpcServer implements ServerMethodInvokerRegistry, AutoCloseable { + + private final NettyRemotingServer nettyRemotingServer; + + public RpcServer(NettyServerConfig nettyServerConfig) { + this.nettyRemotingServer = NettyRemotingServerFactory.buildNettyRemotingServer(nettyServerConfig); + } + + public void start() { + nettyRemotingServer.start(); + } + + @Override + public void registerServerMethodInvokerProvider(Object serverMethodInvokerProviderBean) { + for (Class anInterface : serverMethodInvokerProviderBean.getClass().getInterfaces()) { + if (anInterface.getAnnotation(RpcService.class) == null) { + continue; + } + for (Method method : anInterface.getDeclaredMethods()) { + RpcMethod rpcMethod = method.getAnnotation(RpcMethod.class); + if (rpcMethod == null) { + continue; + } + ServerMethodInvoker serverMethodInvoker = + new ServerMethodInvokerImpl(serverMethodInvokerProviderBean, method); + nettyRemotingServer.registerMethodInvoker(serverMethodInvoker); + log.debug("Register ServerMethodInvoker: {} to bean: {}", + serverMethodInvoker.getMethodIdentify(), serverMethodInvoker.getMethodProviderIdentify()); + } + } + } + + @Override + public void close() { + nettyRemotingServer.close(); + } +} diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvoker.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvoker.java index ee633217b298..151b54bb9750 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvoker.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvoker.java @@ -17,10 +17,12 @@ package org.apache.dolphinscheduler.extract.base.server; -public interface ServerMethodInvoker { +interface ServerMethodInvoker { String getMethodIdentify(); + String getMethodProviderIdentify(); + Object invoke(final Object... arg) throws Throwable; } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvokerImpl.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvokerImpl.java index eea9da5e14a2..4c29650aa030 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvokerImpl.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvokerImpl.java @@ -20,7 +20,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public class ServerMethodInvokerImpl implements ServerMethodInvoker { +class ServerMethodInvokerImpl implements ServerMethodInvoker { private final Object serviceBean; @@ -28,7 +28,7 @@ public class ServerMethodInvokerImpl implements ServerMethodInvoker { private final String methodIdentify; - public ServerMethodInvokerImpl(Object serviceBean, Method method) { + ServerMethodInvokerImpl(Object serviceBean, Method method) { this.serviceBean = serviceBean; this.method = method; this.methodIdentify = method.toGenericString(); @@ -48,4 +48,9 @@ public Object invoke(Object... args) throws Throwable { public String getMethodIdentify() { return methodIdentify; } + + @Override + public String getMethodProviderIdentify() { + return serviceBean.getClass().getName(); + } } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvokerRegistry.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvokerRegistry.java new file mode 100644 index 000000000000..4e56be26174b --- /dev/null +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/ServerMethodInvokerRegistry.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.dolphinscheduler.extract.base.server; + +interface ServerMethodInvokerRegistry { + + /** + * Register service object, which will be used to invoke the {@link ServerMethodInvoker}. + * The serverMethodInvokerProviderObject should implement with interface which contains {@link org.apache.dolphinscheduler.extract.base.RpcService} annotation. + */ + void registerServerMethodInvokerProvider(Object serverMethodInvokerProviderObject); + +} diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/SpringServerMethodInvokerDiscovery.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/SpringServerMethodInvokerDiscovery.java index 2b87a70080fc..de4943990cef 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/SpringServerMethodInvokerDiscovery.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/main/java/org/apache/dolphinscheduler/extract/base/server/SpringServerMethodInvokerDiscovery.java @@ -17,11 +17,7 @@ package org.apache.dolphinscheduler.extract.base.server; -import org.apache.dolphinscheduler.extract.base.NettyRemotingServer; -import org.apache.dolphinscheduler.extract.base.RpcMethod; -import org.apache.dolphinscheduler.extract.base.RpcService; - -import java.lang.reflect.Method; +import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; import lombok.extern.slf4j.Slf4j; @@ -29,38 +25,21 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.lang.Nullable; +/** + * The RpcServer which will auto discovery the {@link ServerMethodInvoker} from Spring container. + */ @Slf4j -public class SpringServerMethodInvokerDiscovery implements BeanPostProcessor { +public class SpringServerMethodInvokerDiscovery extends RpcServer implements BeanPostProcessor { - protected final NettyRemotingServer nettyRemotingServer; - - public SpringServerMethodInvokerDiscovery(NettyRemotingServer nettyRemotingServer) { - this.nettyRemotingServer = nettyRemotingServer; + public SpringServerMethodInvokerDiscovery(NettyServerConfig nettyServerConfig) { + super(nettyServerConfig); } @Nullable @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - Class[] interfaces = bean.getClass().getInterfaces(); - for (Class anInterface : interfaces) { - if (anInterface.getAnnotation(RpcService.class) == null) { - continue; - } - registerRpcMethodInvoker(anInterface, bean, beanName); - } + registerServerMethodInvokerProvider(bean); return bean; } - private void registerRpcMethodInvoker(Class anInterface, Object bean, String beanName) { - Method[] declaredMethods = anInterface.getDeclaredMethods(); - for (Method method : declaredMethods) { - RpcMethod rpcMethod = method.getAnnotation(RpcMethod.class); - if (rpcMethod == null) { - continue; - } - ServerMethodInvoker methodInvoker = new ServerMethodInvokerImpl(bean, method); - nettyRemotingServer.registerMethodInvoker(methodInvoker); - log.debug("Register ServerMethodInvoker: {} to bean: {}", methodInvoker.getMethodIdentify(), beanName); - } - } } diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/test/java/org/apache/dolphinscheduler/extract/base/client/SingletonJdkDynamicRpcClientProxyFactoryTest.java b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/test/java/org/apache/dolphinscheduler/extract/base/client/SingletonJdkDynamicRpcClientProxyFactoryTest.java index 521cf7c75a14..92ed49934cc2 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-base/src/test/java/org/apache/dolphinscheduler/extract/base/client/SingletonJdkDynamicRpcClientProxyFactoryTest.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-base/src/test/java/org/apache/dolphinscheduler/extract/base/client/SingletonJdkDynamicRpcClientProxyFactoryTest.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; -import org.apache.dolphinscheduler.extract.base.NettyRemotingServer; import org.apache.dolphinscheduler.extract.base.RpcMethod; import org.apache.dolphinscheduler.extract.base.RpcService; import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; @@ -37,7 +36,7 @@ public class SingletonJdkDynamicRpcClientProxyFactoryTest { - private NettyRemotingServer nettyRemotingServer; + private SpringServerMethodInvokerDiscovery springServerMethodInvokerDiscovery; private String serverAddress; @@ -48,11 +47,10 @@ public void setUp() { .serverName("ApiServer") .listenPort(listenPort) .build(); - nettyRemotingServer = new NettyRemotingServer(nettyServerConfig); - nettyRemotingServer.start(); serverAddress = "localhost:" + listenPort; - new SpringServerMethodInvokerDiscovery(nettyRemotingServer) - .postProcessAfterInitialization(new IServiceImpl(), "iServiceImpl"); + springServerMethodInvokerDiscovery = new SpringServerMethodInvokerDiscovery(nettyServerConfig); + springServerMethodInvokerDiscovery.registerServerMethodInvokerProvider(new IServiceImpl()); + springServerMethodInvokerDiscovery.start(); } @Test @@ -82,7 +80,7 @@ public void testVoid() { @AfterEach public void tearDown() { - nettyRemotingServer.close(); + springServerMethodInvokerDiscovery.close(); } @RpcService diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/rpc/MasterRpcServer.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/rpc/MasterRpcServer.java index 0eaf885d11bf..ab89b021d618 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/rpc/MasterRpcServer.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/rpc/MasterRpcServer.java @@ -17,7 +17,6 @@ package org.apache.dolphinscheduler.server.master.rpc; -import org.apache.dolphinscheduler.extract.base.NettyRemotingServerFactory; import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; import org.apache.dolphinscheduler.extract.base.server.SpringServerMethodInvokerDiscovery; import org.apache.dolphinscheduler.server.master.config.MasterConfig; @@ -31,21 +30,8 @@ public class MasterRpcServer extends SpringServerMethodInvokerDiscovery implements AutoCloseable { public MasterRpcServer(MasterConfig masterConfig) { - super(NettyRemotingServerFactory.buildNettyRemotingServer(NettyServerConfig.builder() - .serverName("MasterRpcServer").listenPort(masterConfig.getListenPort()).build())); - } - - public void start() { - log.info("Starting MasterRPCServer..."); - nettyRemotingServer.start(); - log.info("Started MasterRPCServer..."); - } - - @Override - public void close() { - log.info("Closing MasterRPCServer..."); - nettyRemotingServer.close(); - log.info("Closed MasterRPCServer..."); + super(NettyServerConfig.builder().serverName("MasterRpcServer").listenPort(masterConfig.getListenPort()) + .build()); } } diff --git a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/rpc/MasterRpcServerTest.java b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/rpc/MasterRpcServerTest.java new file mode 100644 index 000000000000..1e5a77edb332 --- /dev/null +++ b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/rpc/MasterRpcServerTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.server.master.rpc; + +import org.apache.dolphinscheduler.server.master.config.MasterConfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class MasterRpcServerTest { + + private final MasterRpcServer masterRpcServer = new MasterRpcServer(new MasterConfig()); + + @Test + void testStart() { + Assertions.assertDoesNotThrow(masterRpcServer::start); + } + + @Test + void testClose() { + Assertions.assertDoesNotThrow(masterRpcServer::close); + } +} diff --git a/dolphinscheduler-microbench/src/main/java/org/apache/dolphinscheduler/microbench/rpc/RpcBenchMarkTest.java b/dolphinscheduler-microbench/src/main/java/org/apache/dolphinscheduler/microbench/rpc/RpcBenchMarkTest.java index 1a3e4ab1e2a1..496983118fd8 100644 --- a/dolphinscheduler-microbench/src/main/java/org/apache/dolphinscheduler/microbench/rpc/RpcBenchMarkTest.java +++ b/dolphinscheduler-microbench/src/main/java/org/apache/dolphinscheduler/microbench/rpc/RpcBenchMarkTest.java @@ -17,7 +17,6 @@ package org.apache.dolphinscheduler.microbench.rpc; -import org.apache.dolphinscheduler.extract.base.NettyRemotingServer; import org.apache.dolphinscheduler.extract.base.client.SingletonJdkDynamicRpcClientProxyFactory; import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; import org.apache.dolphinscheduler.extract.base.server.SpringServerMethodInvokerDiscovery; @@ -46,18 +45,17 @@ @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime}) public class RpcBenchMarkTest extends AbstractBaseBenchmark { - private NettyRemotingServer nettyRemotingServer; + private SpringServerMethodInvokerDiscovery springServerMethodInvokerDiscovery; private IService iService; @Setup public void before() { - nettyRemotingServer = new NettyRemotingServer( - NettyServerConfig.builder().serverName("NettyRemotingServer").listenPort(12345).build()); - nettyRemotingServer.start(); - SpringServerMethodInvokerDiscovery springServerMethodInvokerDiscovery = - new SpringServerMethodInvokerDiscovery(nettyRemotingServer); + NettyServerConfig nettyServerConfig = + NettyServerConfig.builder().serverName("NettyRemotingServer").listenPort(12345).build(); + springServerMethodInvokerDiscovery = new SpringServerMethodInvokerDiscovery(nettyServerConfig); springServerMethodInvokerDiscovery.postProcessAfterInitialization(new IServiceImpl(), "iServiceImpl"); + springServerMethodInvokerDiscovery.start(); iService = SingletonJdkDynamicRpcClientProxyFactory.getProxyClient("localhost:12345", IService.class); } @@ -72,6 +70,6 @@ public void sendTest(Blackhole bh) { @TearDown public void after() { - nettyRemotingServer.close(); + springServerMethodInvokerDiscovery.close(); } } diff --git a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/rpc/WorkerRpcServer.java b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/rpc/WorkerRpcServer.java index 7733fbba4f66..b9f3855cf952 100644 --- a/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/rpc/WorkerRpcServer.java +++ b/dolphinscheduler-worker/src/main/java/org/apache/dolphinscheduler/server/worker/rpc/WorkerRpcServer.java @@ -17,7 +17,6 @@ package org.apache.dolphinscheduler.server.worker.rpc; -import org.apache.dolphinscheduler.extract.base.NettyRemotingServerFactory; import org.apache.dolphinscheduler.extract.base.config.NettyServerConfig; import org.apache.dolphinscheduler.extract.base.server.SpringServerMethodInvokerDiscovery; import org.apache.dolphinscheduler.server.worker.config.WorkerConfig; @@ -33,21 +32,8 @@ public class WorkerRpcServer extends SpringServerMethodInvokerDiscovery implements Closeable { public WorkerRpcServer(WorkerConfig workerConfig) { - super(NettyRemotingServerFactory.buildNettyRemotingServer(NettyServerConfig.builder() - .serverName("WorkerRpcServer").listenPort(workerConfig.getListenPort()).build())); - } - - public void start() { - log.info("WorkerRpcServer starting..."); - nettyRemotingServer.start(); - log.info("WorkerRpcServer started..."); - } - - @Override - public void close() { - log.info("WorkerRpcServer closing"); - nettyRemotingServer.close(); - log.info("WorkerRpcServer closed"); + super(NettyServerConfig.builder().serverName("WorkerRpcServer").listenPort(workerConfig.getListenPort()) + .build()); } } diff --git a/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/rpc/WorkerRpcServerTest.java b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/rpc/WorkerRpcServerTest.java new file mode 100644 index 000000000000..d27eaeeadfab --- /dev/null +++ b/dolphinscheduler-worker/src/test/java/org/apache/dolphinscheduler/server/worker/rpc/WorkerRpcServerTest.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.server.worker.rpc; + +import org.apache.dolphinscheduler.server.worker.config.WorkerConfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class WorkerRpcServerTest { + + private final WorkerRpcServer workerRpcServer = new WorkerRpcServer(new WorkerConfig()); + + @Test + void testStart() { + Assertions.assertDoesNotThrow(workerRpcServer::start); + } + + @Test + void testClose() { + Assertions.assertDoesNotThrow(workerRpcServer::close); + } + +} From c5f64c76d6fab50140ce1e290307df098a842559 Mon Sep 17 00:00:00 2001 From: ikiler Date: Thu, 9 May 2024 10:05:24 +0800 Subject: [PATCH 063/165] formate doc code --- docs/docs/en/guide/task/dinky.md | 10 +++++----- docs/docs/zh/guide/task/dinky.md | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/docs/en/guide/task/dinky.md b/docs/docs/en/guide/task/dinky.md index 3c11594060b5..b758fa8691e2 100644 --- a/docs/docs/en/guide/task/dinky.md +++ b/docs/docs/en/guide/task/dinky.md @@ -17,11 +17,11 @@ it will call `Dinky API` to trigger dinky task. Click [here](http://www.dlink.to - Please refer to [DolphinScheduler Task Parameters Appendix](appendix.md) `Default Task Parameters` section for default parameters. -| **Parameter** | **Description** | -|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Dinky Address | The URL for the Dinky service, e.g., http://localhost:8888. | -| Dinky Task ID | The unique task id for a dinky task. | -| Online Task | Specify whether the current dinky job is online. If yes, the submitted job can only be submitted successfully when it is published and there is no corresponding Flink job instance running. | +| **Parameter** | **Description** | +|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Dinky Address | The URL for the Dinky service, e.g., http://localhost:8888. | +| Dinky Task ID | The unique task id for a dinky task. | +| Online Task | Specify whether the current dinky job is online. If yes, the submitted job can only be submitted successfully when it is published and there is no corresponding Flink job instance running. | | Custom Parameters | Starting from Dinky 1.0, support for passing custom parameters is available. Currently, only `IN` type inputs are supported, with no support for `OUT` type outputs. Supports the `${param}` syntax for retrieving global or local dynamic parameters. | ## Task Example diff --git a/docs/docs/zh/guide/task/dinky.md b/docs/docs/zh/guide/task/dinky.md index c5c23f51304b..6b7ab3cc4bbb 100644 --- a/docs/docs/zh/guide/task/dinky.md +++ b/docs/docs/zh/guide/task/dinky.md @@ -17,13 +17,12 @@ - 默认参数说明请参考[DolphinScheduler任务参数附录](appendix.md)`默认任务参数`一栏。 -| **任务参数** | **描述** | -|----------|----------------------------------------------------------------------------| -| Dinky 地址 | Dinky 服务的 url,例如:`http://localhost:8888`。 | +| **任务参数** | **描述** | +|-------------|----------------------------------------------------------------------------| +| Dinky 地址 | Dinky 服务的 url,例如:`http://localhost:8888`。 | | Dinky 任务 ID | Dinky 作业对应的唯一ID。 | -| 上线作业 | 指定当前 Dinky 作业是否上线,如果是,则该被提交的作业只能处于已发布且当前无对应的 Flink Job 实例在运行才可提交成功。 | -| 自定义参数 | 从Dinky 1.0开始支持传递自定义参数,目前仅支持`IN`类型输入,不支持`OUT`类型输出。支持`${param}`方式获取全局或局部动态参数 | - +| 上线作业 | 指定当前 Dinky 作业是否上线,如果是,则该被提交的作业只能处于已发布且当前无对应的 Flink Job 实例在运行才可提交成功。 | +| 自定义参数 | 从Dinky 1.0开始支持传递自定义参数,目前仅支持`IN`类型输入,不支持`OUT`类型输出。支持`${param}`方式获取全局或局部动态参数 | ## Task Example From 8426d2346cde5431ad83a23cbce20864026c556a Mon Sep 17 00:00:00 2001 From: xiangzihao <460888207@qq.com> Date: Thu, 9 May 2024 11:01:05 +0800 Subject: [PATCH 064/165] [HotFix] [CI] Temporary skipping mergeable check (#15958) * temporary skipping mergeable check --- .asf.yaml | 2 +- .github/mergeable.yml | 62 -------------------------- .github/workflows/mergeable.yml | 78 +++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 63 deletions(-) delete mode 100644 .github/mergeable.yml create mode 100644 .github/workflows/mergeable.yml diff --git a/.asf.yaml b/.asf.yaml index 84619447beee..abeb08c0b845 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -46,7 +46,7 @@ github: - E2E - Docs - Frontend Build - - "Mergeable: milestone-label-check" +# - "Mergeable: milestone-label-check" required_pull_request_reviews: dismiss_stale_reviews: true required_approving_review_count: 2 diff --git a/.github/mergeable.yml b/.github/mergeable.yml deleted file mode 100644 index a1df2e7410b0..000000000000 --- a/.github/mergeable.yml +++ /dev/null @@ -1,62 +0,0 @@ -# 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: 2 -mergeable: - # we can not use `pull_request.*` which including event `pull_request.labeled`, according to https://github.com/mergeability/mergeable/issues/643, - # otherwise mergeable will keep add or remove label endless, we just need this CI act like the default behavior as - # GitHub action workflow `pull_requests` https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request like, - # which only trigger runs when a pull_request event's activity type is opened, synchronize, or reopened - - when: pull_request.opened, pull_request.reopened, pull_request.synchronize - name: sync-sql-ddl - validate: - # Sql files must change synchronize - - do: dependent - files: - - 'dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql' - - 'dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql' - - 'dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql' - message: 'Sql files not change synchronize' - # Add labels 'sql not sync' and comment to reviewers if Sql files not change synchronize - fail: - - do: comment - payload: - body: > - :warning: This PR do not change database DDL synchronize. - leave_old_comment: false - - do: labels - add: 'sql not sync' - # Remove labels 'sql not sync' if pass - pass: - - do: labels - delete: 'sql not sync' - - - when: pull_request.* - name: milestone-label-check - validate: - - do: milestone - no_empty: - enabled: false # Cannot be empty when true. - message: 'Milestone is required and cannot be empty.' - - do: label - and: - - must_include: - regex: 'feature|bug|improvement|document|chore|revert' - message: 'Label must include one of the following: `feature`, `bug`, `improvement`, `document`, `chore`, `revert`' - - must_include: - regex: 'ready-to-merge' - message: 'Please check if there are PRs that already have a `ready-to-merge` label and can be merged, if exists please merge them first.' diff --git a/.github/workflows/mergeable.yml b/.github/workflows/mergeable.yml new file mode 100644 index 000000000000..8b7bd8799c4a --- /dev/null +++ b/.github/workflows/mergeable.yml @@ -0,0 +1,78 @@ +# 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: 2 + +on: + pull_request: + +name: "Mergeable" + +jobs: + result: + name: "Mergeable: milestone-label-check" + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Status + run: | + echo "Temporary skipping this check" + +#mergeable: +# # we can not use `pull_request.*` which including event `pull_request.labeled`, according to https://github.com/mergeability/mergeable/issues/643, +# # otherwise mergeable will keep add or remove label endless, we just need this CI act like the default behavior as +# # GitHub action workflow `pull_requests` https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request like, +# # which only trigger runs when a pull_request event's activity type is opened, synchronize, or reopened +# - when: pull_request.opened, pull_request.reopened, pull_request.synchronize +# name: sync-sql-ddl +# validate: +# # Sql files must change synchronize +# - do: dependent +# files: +# - 'dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql' +# - 'dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql' +# - 'dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql' +# message: 'Sql files not change synchronize' +# # Add labels 'sql not sync' and comment to reviewers if Sql files not change synchronize +# fail: +# - do: comment +# payload: +# body: > +# :warning: This PR do not change database DDL synchronize. +# leave_old_comment: false +# - do: labels +# add: 'sql not sync' +# # Remove labels 'sql not sync' if pass +# pass: +# - do: labels +# delete: 'sql not sync' +# +# - when: pull_request.* +# name: milestone-label-check +# validate: +# - do: milestone +# no_empty: +# enabled: false # Cannot be empty when true. +# message: 'Milestone is required and cannot be empty.' +# - do: label +# and: +# - must_include: +# regex: 'feature|bug|improvement|document|chore|revert' +# message: 'Label must include one of the following: `feature`, `bug`, `improvement`, `document`, `chore`, `revert`' +# - must_include: +# regex: 'ready-to-merge' +# message: 'Please check if there are PRs that already have a `ready-to-merge` label and can be merged, if exists please merge them first.' From bbca37d03eb255299a0527f299aa90b96e0e1218 Mon Sep 17 00:00:00 2001 From: privking <43061765+privking@users.noreply.github.com> Date: Thu, 9 May 2024 11:36:56 +0800 Subject: [PATCH 065/165] [FIX] Completed tasks cannot be re-executed in a workflow instance (#15884) * fix bug: Failed to resume stopped workflow instance * Revert "fix bug: Failed to resume stopped workflow instance" This reverts commit 1546e9d5a51178a94bedd18a718b15431355428b. * fix bug : Completed tasks cannot be re-executed in a workflow instance --------- Co-authored-by: Rick Cheng --- .../server/master/runner/WorkflowExecuteRunnable.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/WorkflowExecuteRunnable.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/WorkflowExecuteRunnable.java index eafba17f6965..725b7e000a99 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/WorkflowExecuteRunnable.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/WorkflowExecuteRunnable.java @@ -2120,13 +2120,9 @@ protected void clearDataIfExecuteTask() { workflowInstance.setVarPool(JSONUtils.toJsonString(processProperties)); processInstanceDao.updateById(workflowInstance); - // remove task instance from taskInstanceMap, completeTaskSet, validTaskMap, errorTaskMap - // completeTaskSet remove dependency taskInstanceMap, so the sort can't change - completeTaskSet.removeIf(taskCode -> { - Optional existTaskInstanceOptional = getTaskInstance(taskCode); - return existTaskInstanceOptional - .filter(taskInstance -> dag.containsNode(taskInstance.getTaskCode())).isPresent(); - }); + // remove task instance from taskInstanceMap,taskCodeInstanceMap , completeTaskSet, validTaskMap, errorTaskMap + completeTaskSet.removeIf(dag::containsNode); + taskCodeInstanceMap.entrySet().removeIf(entity -> dag.containsNode(entity.getValue().getTaskCode())); taskInstanceMap.entrySet().removeIf(entry -> dag.containsNode(entry.getValue().getTaskCode())); validTaskMap.entrySet().removeIf(entry -> dag.containsNode(entry.getKey())); errorTaskMap.entrySet().removeIf(entry -> dag.containsNode(entry.getKey())); From 8d336def6140d01bf2d6007014a9689b237baab7 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Thu, 9 May 2024 12:23:01 +0800 Subject: [PATCH 066/165] [DSIP-35][Alert] Refactor the alert thread model (#15932) --- .../plugin/alert/voice/VoiceAlertChannel.java | 2 +- .../plugin/alert/voice/VoiceSender.java | 4 +- .../src/test/java/VoiceSenderTest.java | 2 +- .../alert/api/AlertChannel.java | 2 +- .../dolphinscheduler/alert/api/AlertData.java | 8 - .../alert/api/AlertResult.java | 14 +- .../alert/dingtalk/DingTalkAlertChannel.java | 2 +- .../plugin/alert/dingtalk/DingTalkSender.java | 6 +- .../alert/dingtalk/DingTalkSenderTest.java | 2 +- .../plugin/alert/email/EmailAlertChannel.java | 10 +- .../plugin/alert/email/MailSender.java | 6 +- .../alert/email/EmailAlertChannelTest.java | 2 +- .../plugin/alert/email/MailUtilsTest.java | 14 +- .../alert/feishu/FeiShuAlertChannel.java | 2 +- .../plugin/alert/feishu/FeiShuSender.java | 6 +- .../plugin/alert/feishu/FeiShuSenderTest.java | 6 +- .../plugin/alert/http/HttpAlertChannel.java | 2 +- .../plugin/alert/http/HttpSender.java | 6 +- .../alert/http/HttpAlertChannelTest.java | 4 +- .../plugin/alert/http/HttpSenderTest.java | 2 +- .../pagerduty/PagerDutyAlertChannel.java | 4 +- .../alert/pagerduty/PagerDutySender.java | 4 +- .../alert/pagerduty/PagerDutySenderTest.java | 2 +- .../prometheus/PrometheusAlertChannel.java | 2 +- .../prometheus/PrometheusAlertSender.java | 12 +- .../prometheus/PrometheusAlertSenderTest.java | 6 +- .../alert/script/ScriptAlertChannel.java | 2 +- .../plugin/alert/script/ScriptSender.java | 6 +- .../plugin/alert/script/ScriptSenderTest.java | 16 +- .../plugin/alert/slack/SlackAlertChannel.java | 6 +- .../alert/telegram/TelegramAlertChannel.java | 2 +- .../plugin/alert/telegram/TelegramSender.java | 6 +- .../alert/telegram/TelegramSenderTest.java | 10 +- .../webexteams/WebexTeamsAlertChannel.java | 2 +- .../alert/webexteams/WebexTeamsSender.java | 4 +- .../webexteams/WebexTeamsSenderTest.java | 2 +- .../alert/wechat/WeChatAlertChannel.java | 2 +- .../plugin/alert/wechat/WeChatSender.java | 11 +- .../plugin/alert/wechat/WeChatSenderTest.java | 4 +- .../dolphinscheduler-alert-server/pom.xml | 6 + .../dolphinscheduler/alert/AlertServer.java | 46 +-- .../alert/config/AlertConfig.java | 6 + .../alert/metrics/AlertServerMetrics.java | 6 + .../alert/plugin/AlertPluginManager.java | 2 +- .../alert/registry/AlertHeartbeatTask.java | 8 +- .../alert/registry/AlertRegistryClient.java | 9 +- .../alert/rpc/AlertOperatorImpl.java | 11 +- .../alert/service/AbstractEventFetcher.java | 100 +++++ .../alert/service/AbstractEventLoop.java | 101 +++++ .../service/AbstractEventPendingQueue.java | 53 +++ .../alert/service/AbstractEventSender.java | 191 +++++++++ .../alert/service/AlertBootstrapService.java | 376 +++--------------- .../alert/service/AlertEventFetcher.java | 51 +++ .../alert/service/AlertEventLoop.java | 46 +++ .../alert/service/AlertEventPendingQueue.java | 33 ++ .../alert/service/AlertHAServer.java | 36 ++ .../alert/service/AlertSender.java | 131 ++++++ .../service/AlertSenderThreadPoolFactory.java | 41 ++ .../alert/service/EventFetcher.java | 34 ++ .../alert/service/EventLoop.java | 47 +++ .../alert/service/EventPendingQueue.java | 34 ++ .../alert/service/EventSender.java | 33 ++ .../alert/service/ListenerEventFetcher.java | 51 +++ .../alert/service/ListenerEventLoop.java | 40 ++ .../service/ListenerEventPendingQueue.java | 32 ++ .../service/ListenerEventPostService.java | 262 ------------ .../alert/service/ListenerEventSender.java | 146 +++++++ .../src/main/resources/application.yaml | 3 +- .../alert/config/AlertConfigTest.java | 43 ++ ...pServiceTest.java => AlertSenderTest.java} | 74 +--- ...Test.java => ListenerEventSenderTest.java} | 47 +-- .../service/AlertEventPendingQueueTest.java | 94 +++++ .../AlertSenderThreadPoolFactoryTest.java | 44 ++ .../src/test/resources/application.yaml | 107 +++++ .../common/enums/ServerStatus.java | 2 +- .../common/model/AlertServerHeartBeat.java | 5 + .../apache/dolphinscheduler/dao/AlertDao.java | 35 +- .../dao/mapper/AlertMapper.java | 5 +- .../dao/mapper/ListenerEventMapper.java | 3 +- .../dao/repository/ListenerEventDao.java | 31 ++ .../repository/impl/ListenerEventDaoImpl.java | 51 +++ .../dao/mapper/AlertMapper.xml | 4 +- .../dao/mapper/ListenerEventMapper.xml | 3 +- .../dao/mapper/ListenerEventMapperTest.java | 4 +- .../dao/repository/impl/AlertDaoTest.java | 28 +- .../impl/ListenerEventDaoImplTest.java | 79 ++++ .../alert/request/AlertSendResponse.java | 16 + .../registry/api/Registry.java | 7 +- .../registry/api/ha/AbstractHAServer.java | 105 +++++ .../AbstractServerStatusChangeListener.java | 42 ++ .../ha/DefaultServerStatusChangeListener.java | 34 ++ .../registry/api/ha/HAServer.java | 68 ++++ .../api/ha/ServerStatusChangeListener.java | 24 ++ .../plugin/registry/etcd/EtcdRegistry.java | 31 ++ .../etcd/EtcdKeepAliveLeaseManagerTest.java | 6 +- .../plugin/registry/jdbc/JdbcRegistry.java | 11 + .../jdbc/task/RegistryLockManager.java | 24 ++ .../registry/zookeeper/ZookeeperRegistry.java | 48 ++- .../src/main/resources/application.yaml | 3 +- 99 files changed, 2336 insertions(+), 882 deletions(-) create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventFetcher.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventLoop.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventPendingQueue.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventSender.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventFetcher.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventLoop.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventPendingQueue.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertHAServer.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSender.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSenderThreadPoolFactory.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventFetcher.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventLoop.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventPendingQueue.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventSender.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventFetcher.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventLoop.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventPendingQueue.java delete mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventPostService.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventSender.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/config/AlertConfigTest.java rename dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/{AlertBootstrapServiceTest.java => AlertSenderTest.java} (73%) rename dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/{ListenerEventPostServiceTest.java => ListenerEventSenderTest.java} (82%) create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/service/AlertEventPendingQueueTest.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/service/AlertSenderThreadPoolFactoryTest.java create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/resources/application.yaml create mode 100644 dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/ListenerEventDao.java create mode 100644 dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/ListenerEventDaoImpl.java create mode 100644 dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/ListenerEventDaoImplTest.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/AbstractHAServer.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/AbstractServerStatusChangeListener.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/DefaultServerStatusChangeListener.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/HAServer.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/ServerStatusChangeListener.java diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/main/java/org/apache/dolphinscheduler/plugin/alert/voice/VoiceAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/main/java/org/apache/dolphinscheduler/plugin/alert/voice/VoiceAlertChannel.java index eeaaba5d0128..4aa29c19c5ad 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/main/java/org/apache/dolphinscheduler/plugin/alert/voice/VoiceAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/main/java/org/apache/dolphinscheduler/plugin/alert/voice/VoiceAlertChannel.java @@ -40,7 +40,7 @@ public AlertResult process(AlertInfo info) { Map paramsMap = info.getAlertParams(); if (null == paramsMap) { - return new AlertResult("false", "aliyun-voice params is null"); + return new AlertResult(false, "aliyun-voice params is null"); } VoiceParam voiceParam = buildVoiceParam(paramsMap); return new VoiceSender(voiceParam).send(); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/main/java/org/apache/dolphinscheduler/plugin/alert/voice/VoiceSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/main/java/org/apache/dolphinscheduler/plugin/alert/voice/VoiceSender.java index c6c29d8735ec..fe0fc65986f8 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/main/java/org/apache/dolphinscheduler/plugin/alert/voice/VoiceSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/main/java/org/apache/dolphinscheduler/plugin/alert/voice/VoiceSender.java @@ -46,7 +46,7 @@ public VoiceSender(VoiceParam voiceParam) { public AlertResult send() { AlertResult alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); try { Client client = createClient(voiceParam.getConnection()); SingleCallByTtsRequest singleCallByTtsRequest = new SingleCallByTtsRequest() @@ -61,7 +61,7 @@ public AlertResult send() { } SingleCallByTtsResponseBody body = response.getBody(); if (body.code.equalsIgnoreCase("ok")) { - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage(body.getCallId()); } else { alertResult.setMessage(body.getMessage()); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/test/java/VoiceSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/test/java/VoiceSenderTest.java index 515a410b6386..15f4871392cd 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/test/java/VoiceSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/test/java/VoiceSenderTest.java @@ -46,7 +46,7 @@ void testSendWeChatTableMsg() { VoiceSender weChatSender = new VoiceSender(voiceParam); AlertResult alertResult = weChatSender.send(); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertChannel.java index 530a5483425d..a4eaae232fbd 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertChannel.java @@ -35,6 +35,6 @@ public interface AlertChannel { AlertResult process(AlertInfo info); default @NonNull AlertResult closeAlert(AlertInfo info) { - return new AlertResult("true", "no need to close alert"); + return new AlertResult(true, "no need to close alert"); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertData.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertData.java index 37a3f3357c26..004e8b3bdaf2 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertData.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertData.java @@ -53,14 +53,6 @@ public class AlertData { */ private String log; - /** - * 0 do not send warning; - * 1 send if process success; - * 2 send if process failed; - * 3 send if process ends, whatever the result; - */ - private int warnType; - /** * AlertType#code */ diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertResult.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertResult.java index b6c5db38e980..ceeed97510f1 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertResult.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-api/src/main/java/org/apache/dolphinscheduler/alert/api/AlertResult.java @@ -33,15 +33,19 @@ @NoArgsConstructor public class AlertResult { - /** - * todo: use enum - * false or true - */ - private String status; + private boolean success; /** * alert result message, each plugin can have its own message */ private String message; + public static AlertResult success() { + return new AlertResult(true, null); + } + + public static AlertResult fail(String message) { + return new AlertResult(false, message); + } + } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkAlertChannel.java index 74c440fe76c5..f5cc938246fa 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkAlertChannel.java @@ -31,7 +31,7 @@ public AlertResult process(AlertInfo alertInfo) { AlertData alertData = alertInfo.getAlertData(); Map paramsMap = alertInfo.getAlertParams(); if (null == paramsMap) { - return new AlertResult("false", "ding talk params is null"); + return new AlertResult(false, "ding talk params is null"); } return new DingTalkSender(paramsMap).sendDingTalkMsg(alertData.getTitle(), alertData.getContent()); } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSender.java index c8ded8cfadcf..527e38cf7752 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/main/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSender.java @@ -126,7 +126,7 @@ private static RequestConfig getProxyConfig(String proxy, int port) { private AlertResult checkSendDingTalkSendMsgResult(String result) { AlertResult alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); if (null == result) { alertResult.setMessage("send ding talk msg error"); @@ -140,7 +140,7 @@ private AlertResult checkSendDingTalkSendMsgResult(String result) { return alertResult; } if (sendMsgResponse.errcode == 0) { - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage("send ding talk msg success"); return alertResult; } @@ -164,7 +164,7 @@ public AlertResult sendDingTalkMsg(String title, String content) { } catch (Exception e) { log.info("send ding talk alert msg exception : {}", e.getMessage()); alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); alertResult.setMessage("send ding talk alert fail."); } return alertResult; diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSenderTest.java index cd30105c7a33..90f64d7bb230 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/java/org/apache/dolphinscheduler/plugin/alert/dingtalk/DingTalkSenderTest.java @@ -52,7 +52,7 @@ public void testSend() { dingTalkConfig.put(DingTalkParamsConstants.NAME_DING_TALK_PROXY_ENABLE, "true"); dingTalkSender = new DingTalkSender(dingTalkConfig); AlertResult alertResult = dingTalkSender.sendDingTalkMsg("title", "content test"); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertEquals(false, alertResult.isSuccess()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/EmailAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/EmailAlertChannel.java index 5728461ae633..06aecd35db02 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/EmailAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/EmailAlertChannel.java @@ -35,24 +35,20 @@ public AlertResult process(AlertInfo info) { AlertData alert = info.getAlertData(); Map paramsMap = info.getAlertParams(); if (null == paramsMap) { - return new AlertResult("false", "mail params is null"); + return new AlertResult(false, "mail params is null"); } MailSender mailSender = new MailSender(paramsMap); AlertResult alertResult = mailSender.sendMails(alert.getTitle(), alert.getContent()); - boolean flag; - if (alertResult == null) { alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); alertResult.setMessage("alert send error."); log.info("alert send error : {}", alertResult.getMessage()); return alertResult; } - flag = Boolean.parseBoolean(String.valueOf(alertResult.getStatus())); - - if (flag) { + if (alertResult.isSuccess()) { log.info("alert send success"); alertResult.setMessage("email send success."); } else { diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/MailSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/MailSender.java index 8826a44fbac7..58e1eb10b3aa 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/MailSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/main/java/org/apache/dolphinscheduler/plugin/alert/email/MailSender.java @@ -154,7 +154,7 @@ public AlertResult sendMails(String title, String content) { */ public AlertResult sendMails(List receivers, List receiverCcs, String title, String content) { AlertResult alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); // if there is no receivers && no receiversCc, no need to process if (CollectionUtils.isEmpty(receivers) && CollectionUtils.isEmpty(receiverCcs)) { @@ -201,7 +201,7 @@ public AlertResult sendMails(List receivers, List receiverCcs, S attachment(title, content, partContent); - alertResult.setStatus("true"); + alertResult.setSuccess(true); return alertResult; } catch (Exception e) { handleException(alertResult, e); @@ -380,7 +380,7 @@ private AlertResult getStringObjectMap(String title, String content, AlertResult email.setDebug(true); email.send(); - alertResult.setStatus("true"); + alertResult.setSuccess(true); return alertResult; } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/EmailAlertChannelTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/EmailAlertChannelTest.java index 9df19154aade..643cd8a01ea3 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/EmailAlertChannelTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/EmailAlertChannelTest.java @@ -66,7 +66,7 @@ public void testProcess() { alertInfo.setAlertParams(paramsMap); AlertResult alertResult = emailAlertChannel.process(alertInfo); Assertions.assertNotNull(alertResult); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } public String getEmailAlertParams() { diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/MailUtilsTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/MailUtilsTest.java index acc255ae0e9a..9a4b5e82579f 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/MailUtilsTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/java/org/apache/dolphinscheduler/plugin/alert/email/MailUtilsTest.java @@ -77,7 +77,7 @@ public void testSendMails() { AlertResult alertResult = mailSender.sendMails( "Mysql Exception", content); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } @Test @@ -107,7 +107,7 @@ void testAuthCheck() { emailConfig.put(MailParamsConstants.NAME_MAIL_PASSWD, "passwd"); mailSender = new MailSender(emailConfig); AlertResult alertResult = mailSender.sendMails(title, content); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } public String list2String() { @@ -142,24 +142,24 @@ public void testSendTableMail() { emailConfig.put(AlertConstants.NAME_SHOW_TYPE, ShowType.TABLE.getDescp()); mailSender = new MailSender(emailConfig); AlertResult alertResult = mailSender.sendMails(title, content); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } @Test - public void testAttachmentFile() throws Exception { + public void testAttachmentFile() { String content = list2String(); emailConfig.put(AlertConstants.NAME_SHOW_TYPE, ShowType.ATTACHMENT.getDescp()); mailSender = new MailSender(emailConfig); AlertResult alertResult = mailSender.sendMails("gaojing", content); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } @Test - public void testTableAttachmentFile() throws Exception { + public void testTableAttachmentFile() { String content = list2String(); emailConfig.put(AlertConstants.NAME_SHOW_TYPE, ShowType.TABLE_ATTACHMENT.getDescp()); mailSender = new MailSender(emailConfig); AlertResult alertResult = mailSender.sendMails("gaojing", content); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuAlertChannel.java index 8959c8aaec13..29c78a9d1bb6 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuAlertChannel.java @@ -31,7 +31,7 @@ public AlertResult process(AlertInfo alertInfo) { AlertData alertData = alertInfo.getAlertData(); Map paramsMap = alertInfo.getAlertParams(); if (null == paramsMap) { - return new AlertResult("false", "fei shu params is null"); + return new AlertResult(false, "fei shu params is null"); } return new FeiShuSender(paramsMap).sendFeiShuMsg(alertData); } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSender.java index 369060843c1e..1c2f3656ea08 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/main/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSender.java @@ -80,7 +80,7 @@ private static String textToJsonString(AlertData alertData) { public static AlertResult checkSendFeiShuSendMsgResult(String result) { AlertResult alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); if (org.apache.commons.lang3.StringUtils.isBlank(result)) { alertResult.setMessage("send fei shu msg error"); @@ -95,7 +95,7 @@ public static AlertResult checkSendFeiShuSendMsgResult(String result) { return alertResult; } if (sendMsgResponse.statusCode == 0) { - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage("send fei shu msg success"); return alertResult; } @@ -136,7 +136,7 @@ public AlertResult sendFeiShuMsg(AlertData alertData) { } catch (Exception e) { log.info("send fei shu alert msg exception : {}", e.getMessage()); alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); alertResult.setMessage("send fei shu alert fail."); } return alertResult; diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/test/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/test/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSenderTest.java index 41f372b85c12..829b02dea655 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/test/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/test/java/org/apache/dolphinscheduler/plugin/alert/feishu/FeiShuSenderTest.java @@ -43,7 +43,7 @@ public void testSend() { alertData.setContent("feishu test content"); FeiShuSender feiShuSender = new FeiShuSender(feiShuConfig); AlertResult alertResult = feiShuSender.sendFeiShuMsg(alertData); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } @Test @@ -87,12 +87,12 @@ public void testCheckSendFeiShuSendMsgResult() { FeiShuSender feiShuSender = new FeiShuSender(feiShuConfig); AlertResult alertResult = feiShuSender.checkSendFeiShuSendMsgResult(""); - Assertions.assertFalse(Boolean.valueOf(alertResult.getStatus())); + Assertions.assertFalse(alertResult.isSuccess()); AlertResult alertResult2 = feiShuSender.checkSendFeiShuSendMsgResult("123"); Assertions.assertEquals("send fei shu msg fail", alertResult2.getMessage()); String response = "{\"StatusCode\":\"0\",\"extra\":\"extra\",\"StatusMessage\":\"StatusMessage\"}"; AlertResult alertResult3 = feiShuSender.checkSendFeiShuSendMsgResult(response); - Assertions.assertTrue(Boolean.valueOf(alertResult3.getStatus())); + Assertions.assertTrue(alertResult3.isSuccess()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpAlertChannel.java index 944762f13f22..caf1c4c59835 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpAlertChannel.java @@ -31,7 +31,7 @@ public AlertResult process(AlertInfo alertInfo) { AlertData alertData = alertInfo.getAlertData(); Map paramsMap = alertInfo.getAlertParams(); if (null == paramsMap) { - return new AlertResult("false", "http params is null"); + return new AlertResult(false, "http params is null"); } return new HttpSender(paramsMap).send(alertData.getContent()); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSender.java index a1de852407ca..e2a6606a3938 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/main/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSender.java @@ -92,18 +92,18 @@ public AlertResult send(String msg) { } if (httpRequest == null) { - alertResult.setStatus("false"); + alertResult.setSuccess(false); alertResult.setMessage("Request types are not supported"); return alertResult; } try { String resp = this.getResponseString(httpRequest); - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage(resp); } catch (Exception e) { log.error("send http alert msg exception : {}", e.getMessage()); - alertResult.setStatus("false"); + alertResult.setSuccess(false); alertResult.setMessage( String.format("Send http request alert failed: %s", e.getMessage())); } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/java/org/apache/dolphinscheduler/plugin/alert/http/HttpAlertChannelTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/java/org/apache/dolphinscheduler/plugin/alert/http/HttpAlertChannelTest.java index aebf6f9d50bf..ee67db47f1fb 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/java/org/apache/dolphinscheduler/plugin/alert/http/HttpAlertChannelTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/java/org/apache/dolphinscheduler/plugin/alert/http/HttpAlertChannelTest.java @@ -62,9 +62,9 @@ public void testProcessSuccess() { // HttpSender(paramsMap).send(alertData.getContent()); already test in HttpSenderTest.sendTest. so we can mock // it - doReturn(new AlertResult("true", "success")).when(alertChannel).process(any()); + doReturn(new AlertResult(true, "success")).when(alertChannel).process(any()); AlertResult alertResult = alertChannel.process(alertInfo); - Assertions.assertEquals("true", alertResult.getStatus()); + Assertions.assertTrue(alertResult.isSuccess()); } /** diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSenderTest.java index be013457ac29..40f589a10b9d 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/java/org/apache/dolphinscheduler/plugin/alert/http/HttpSenderTest.java @@ -46,7 +46,7 @@ public void sendTest() throws Exception { HttpSender httpSender = spy(new HttpSender(paramsMap)); doReturn("success").when(httpSender).getResponseString(any()); AlertResult alertResult = httpSender.send("Fault tolerance warning"); - Assertions.assertEquals("true", alertResult.getStatus()); + Assertions.assertTrue(alertResult.isSuccess()); Assertions.assertTrue(httpSender.getRequestUrl().contains(url)); Assertions.assertTrue(httpSender.getRequestUrl().contains(contentField)); } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/main/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutyAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/main/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutyAlertChannel.java index b03313952072..430bacbf63a3 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/main/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutyAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/main/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutyAlertChannel.java @@ -30,8 +30,8 @@ public final class PagerDutyAlertChannel implements AlertChannel { public AlertResult process(AlertInfo alertInfo) { AlertData alertData = alertInfo.getAlertData(); Map alertParams = alertInfo.getAlertParams(); - if (alertParams == null || alertParams.size() == 0) { - return new AlertResult("false", "PagerDuty alert params is empty"); + if (alertParams == null || alertParams.isEmpty()) { + return new AlertResult(false, "PagerDuty alert params is empty"); } return new PagerDutySender(alertParams).sendPagerDutyAlter(alertData.getTitle(), alertData.getContent()); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/main/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutySender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/main/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutySender.java index 65792c8eae10..11dd01048ab8 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/main/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutySender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/main/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutySender.java @@ -53,7 +53,7 @@ public PagerDutySender(Map config) { public AlertResult sendPagerDutyAlter(String title, String content) { AlertResult alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); alertResult.setMessage("send pager duty alert fail."); try { @@ -83,7 +83,7 @@ private AlertResult send(AlertResult alertResult, String url, String requestBody String responseContent = EntityUtils.toString(entity, StandardCharsets.UTF_8); try { if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_ACCEPTED) { - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage("send pager duty alert success"); } else { alertResult.setMessage( diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/test/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutySenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/test/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutySenderTest.java index 16cf16f62f7c..52a47aa20e9c 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/test/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutySenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/test/java/org/apache/dolphinscheduler/plugin/alert/pagerduty/PagerDutySenderTest.java @@ -39,6 +39,6 @@ public void initDingTalkConfig() { public void testSend() { PagerDutySender pagerDutySender = new PagerDutySender(pagerDutyConfig); AlertResult alertResult = pagerDutySender.sendPagerDutyAlter("pagerduty test title", "pagerduty test content"); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertChannel.java index 7ca79253fadf..5928faacec2b 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertChannel.java @@ -31,7 +31,7 @@ public AlertResult process(AlertInfo info) { AlertData alertData = info.getAlertData(); Map paramsMap = info.getAlertParams(); if (null == paramsMap) { - return new AlertResult("false", "prometheus alert manager params is null"); + return new AlertResult(false, "prometheus alert manager params is null"); } return new PrometheusAlertSender(paramsMap).sendMessage(alertData); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSender.java index 1106e6799f11..48fda566b105 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/main/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSender.java @@ -23,6 +23,7 @@ import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; @@ -64,11 +65,10 @@ public AlertResult sendMessage(AlertData alertData) { String resp = sendMsg(alertData); return checkSendAlertManageMsgResult(resp); } catch (Exception e) { - String errorMsg = String.format("send prometheus alert manager alert error, exception: %s", e.getMessage()); - log.error(errorMsg); + log.error("Send prometheus alert manager alert error", e); alertResult = new AlertResult(); - alertResult.setStatus("false"); - alertResult.setMessage(errorMsg); + alertResult.setSuccess(false); + alertResult.setMessage(ExceptionUtils.getMessage(e)); } return alertResult; } @@ -106,10 +106,10 @@ private String sendMsg(AlertData alertData) throws IOException { public AlertResult checkSendAlertManageMsgResult(String resp) { AlertResult alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); if (Objects.equals(resp, PrometheusAlertConstants.ALERT_SUCCESS)) { - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage("prometheus alert manager send success"); return alertResult; } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/test/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/test/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSenderTest.java index 2347d9726280..c0d18396e43b 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/test/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/test/java/org/apache/dolphinscheduler/plugin/alert/prometheus/PrometheusAlertSenderTest.java @@ -55,17 +55,17 @@ public void testSendAlert() { " }]"); PrometheusAlertSender sender = new PrometheusAlertSender(config); AlertResult result = sender.sendMessage(alertData); - Assertions.assertEquals("false", result.getStatus()); + Assertions.assertFalse(result.isSuccess()); } @Test public void testCheckSendAlertManageMsgResult() { PrometheusAlertSender prometheusAlertSender = new PrometheusAlertSender(config); AlertResult alertResult1 = prometheusAlertSender.checkSendAlertManageMsgResult(""); - Assertions.assertFalse(Boolean.parseBoolean(alertResult1.getStatus())); + Assertions.assertFalse(alertResult1.isSuccess()); Assertions.assertEquals("prometheus alert manager send fail, resp is ", alertResult1.getMessage()); AlertResult alertResult2 = prometheusAlertSender.checkSendAlertManageMsgResult("alert success"); - Assertions.assertTrue(Boolean.parseBoolean(alertResult2.getStatus())); + Assertions.assertTrue(alertResult2.isSuccess()); Assertions.assertEquals("prometheus alert manager send success", alertResult2.getMessage()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/main/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/main/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptAlertChannel.java index d091eb9d8271..81cd59a5a9be 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/main/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/main/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptAlertChannel.java @@ -33,7 +33,7 @@ public AlertResult process(AlertInfo alertinfo) { AlertData alertData = alertinfo.getAlertData(); Map paramsMap = alertinfo.getAlertParams(); if (MapUtils.isEmpty(paramsMap)) { - return new AlertResult("false", "script params is empty"); + return new AlertResult(false, "script params is empty"); } return new ScriptSender(paramsMap).sendScriptAlert(alertData.getTitle(), alertData.getContent()); } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/main/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/main/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptSender.java index a18adb2c7e2a..19b7149e74ee 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/main/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/main/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptSender.java @@ -56,7 +56,7 @@ AlertResult sendScriptAlert(String title, String content) { } // If it is another type of alarm script can be added here, such as python - alertResult.setStatus("false"); + alertResult.setSuccess(false); log.error("script type error: {}", scriptType); alertResult.setMessage("script type error : " + scriptType); return alertResult; @@ -64,7 +64,7 @@ AlertResult sendScriptAlert(String title, String content) { private AlertResult executeShellScript(String title, String content) { AlertResult alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); if (Boolean.TRUE.equals(OSUtils.isWindows())) { alertResult.setMessage("shell script not support windows os"); return alertResult; @@ -111,7 +111,7 @@ private AlertResult executeShellScript(String title, String content) { int exitCode = ProcessUtils.executeScript(cmd); if (exitCode == 0) { - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage("send script alert msg success"); return alertResult; } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptSenderTest.java index 32e996f5e887..c392b6f7587f 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/java/org/apache/dolphinscheduler/plugin/alert/script/ScriptSenderTest.java @@ -17,6 +17,8 @@ package org.apache.dolphinscheduler.plugin.alert.script; +import static org.junit.jupiter.api.Assertions.assertFalse; + import org.apache.dolphinscheduler.alert.api.AlertResult; import java.util.HashMap; @@ -48,9 +50,9 @@ public void testScriptSenderTest() { ScriptSender scriptSender = new ScriptSender(scriptConfig); AlertResult alertResult; alertResult = scriptSender.sendScriptAlert("test title Kris", "test content"); - Assertions.assertEquals("true", alertResult.getStatus()); + Assertions.assertTrue(alertResult.isSuccess()); alertResult = scriptSender.sendScriptAlert("error msg title", "test content"); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } @Test @@ -58,7 +60,7 @@ public void testScriptSenderInjectionTest() { scriptConfig.put(ScriptParamsConstants.NAME_SCRIPT_USER_PARAMS, "' ; calc.exe ; '"); ScriptSender scriptSender = new ScriptSender(scriptConfig); AlertResult alertResult = scriptSender.sendScriptAlert("test title Kris", "test content"); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } @Test @@ -67,7 +69,7 @@ public void testUserParamsNPE() { ScriptSender scriptSender = new ScriptSender(scriptConfig); AlertResult alertResult; alertResult = scriptSender.sendScriptAlert("test user params NPE", "test content"); - Assertions.assertEquals("true", alertResult.getStatus()); + Assertions.assertTrue(alertResult.isSuccess()); } @Test @@ -76,7 +78,7 @@ public void testPathNPE() { ScriptSender scriptSender = new ScriptSender(scriptConfig); AlertResult alertResult; alertResult = scriptSender.sendScriptAlert("test path NPE", "test content"); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } @Test @@ -85,7 +87,7 @@ public void testPathError() { ScriptSender scriptSender = new ScriptSender(scriptConfig); AlertResult alertResult; alertResult = scriptSender.sendScriptAlert("test path NPE", "test content"); - Assertions.assertEquals("false", alertResult.getStatus()); + assertFalse(alertResult.isSuccess()); Assertions.assertTrue(alertResult.getMessage().contains("shell script is invalid, only support .sh file")); } @@ -95,7 +97,7 @@ public void testTypeIsError() { ScriptSender scriptSender = new ScriptSender(scriptConfig); AlertResult alertResult; alertResult = scriptSender.sendScriptAlert("test type is error", "test content"); - Assertions.assertEquals("false", alertResult.getStatus()); + assertFalse(alertResult.isSuccess()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannel.java index c8cb36a78b39..8052c7c4f13b 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannel.java @@ -30,11 +30,11 @@ public final class SlackAlertChannel implements AlertChannel { public AlertResult process(AlertInfo alertInfo) { AlertData alertData = alertInfo.getAlertData(); Map alertParams = alertInfo.getAlertParams(); - if (alertParams == null || alertParams.size() == 0) { - return new AlertResult("false", "Slack alert params is empty"); + if (alertParams == null || alertParams.isEmpty()) { + return new AlertResult(false, "Slack alert params is empty"); } SlackSender slackSender = new SlackSender(alertParams); String response = slackSender.sendMessage(alertData.getTitle(), alertData.getContent()); - return new AlertResult("ok".equals(response) ? "true" : "false", response); + return new AlertResult("ok".equals(response), response); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramAlertChannel.java index efc8912d1a0a..ed33ef549767 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramAlertChannel.java @@ -30,7 +30,7 @@ public final class TelegramAlertChannel implements AlertChannel { public AlertResult process(AlertInfo info) { Map alertParams = info.getAlertParams(); if (alertParams == null || alertParams.isEmpty()) { - return new AlertResult("false", "Telegram alert params is empty"); + return AlertResult.fail("Telegram alert params is empty"); } AlertData data = info.getAlertData(); return new TelegramSender(alertParams).sendMessage(data); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSender.java index 129bc62c1c2d..417e97d4cd2a 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/main/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSender.java @@ -105,7 +105,7 @@ public AlertResult sendMessage(AlertData alertData) { } catch (Exception e) { log.warn("send telegram alert msg exception : {}", e.getMessage()); result = new AlertResult(); - result.setStatus("false"); + result.setSuccess(false); result.setMessage(String.format("send telegram alert fail. %s", e.getMessage())); } return result; @@ -113,7 +113,7 @@ public AlertResult sendMessage(AlertData alertData) { private AlertResult parseRespToResult(String resp) { AlertResult result = new AlertResult(); - result.setStatus("false"); + result.setSuccess(false); if (null == resp || resp.isEmpty()) { result.setMessage("send telegram msg error. telegram server resp is empty"); return result; @@ -127,7 +127,7 @@ private AlertResult parseRespToResult(String resp) { result.setMessage(String.format("send telegram alert fail. telegram server error_code: %d, description: %s", response.errorCode, response.description)); } else { - result.setStatus("true"); + result.setSuccess(true); result.setMessage("send telegram msg success."); } return result; diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/test/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/test/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSenderTest.java index a57de30219d6..d05a45d73f64 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/test/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/test/java/org/apache/dolphinscheduler/plugin/alert/telegram/TelegramSenderTest.java @@ -52,7 +52,7 @@ public void testSendMessageFailByParamToken() { TelegramParamsConstants.NAME_TELEGRAM_BOT_TOKEN, "XXXXXXX"); TelegramSender telegramSender = new TelegramSender(telegramConfig); AlertResult result = telegramSender.sendMessage(alertData); - Assertions.assertEquals("false", result.getStatus()); + Assertions.assertFalse(result.isSuccess()); } @@ -65,7 +65,7 @@ public void testSendMessageFailByChatId() { TelegramParamsConstants.NAME_TELEGRAM_CHAT_ID, "-XXXXXXX"); TelegramSender telegramSender = new TelegramSender(telegramConfig); AlertResult result = telegramSender.sendMessage(alertData); - Assertions.assertEquals("false", result.getStatus()); + Assertions.assertFalse(result.isSuccess()); } @Test @@ -75,7 +75,7 @@ public void testSendMessage() { alertData.setContent("telegram test content"); TelegramSender telegramSender = new TelegramSender(telegramConfig); AlertResult result = telegramSender.sendMessage(alertData); - Assertions.assertEquals("false", result.getStatus()); + Assertions.assertFalse(result.isSuccess()); } @@ -89,7 +89,7 @@ public void testSendMessageByMarkdown() { TelegramParamsConstants.NAME_TELEGRAM_PARSE_MODE, TelegramAlertConstants.PARSE_MODE_MARKDOWN); TelegramSender telegramSender = new TelegramSender(telegramConfig); AlertResult result = telegramSender.sendMessage(alertData); - Assertions.assertEquals("false", result.getStatus()); + Assertions.assertFalse(result.isSuccess()); } @@ -102,7 +102,7 @@ public void testSendMessageByHtml() { TelegramParamsConstants.NAME_TELEGRAM_PARSE_MODE, TelegramAlertConstants.PARSE_MODE_HTML); TelegramSender telegramSender = new TelegramSender(telegramConfig); AlertResult result = telegramSender.sendMessage(alertData); - Assertions.assertEquals("false", result.getStatus()); + Assertions.assertFalse(result.isSuccess()); } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/main/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/main/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsAlertChannel.java index 38a582f1c63d..94f77aed6e04 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/main/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/main/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsAlertChannel.java @@ -33,7 +33,7 @@ public AlertResult process(AlertInfo alertInfo) { AlertData alertData = alertInfo.getAlertData(); Map alertParams = alertInfo.getAlertParams(); if (MapUtils.isEmpty(alertParams)) { - return new AlertResult("false", "WebexTeams alert params is empty"); + return new AlertResult(false, "WebexTeams alert params is empty"); } return new WebexTeamsSender(alertParams).sendWebexTeamsAlter(alertData); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/main/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/main/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsSender.java index f8201a40e096..3b8b3d21c818 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/main/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/main/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsSender.java @@ -67,7 +67,7 @@ public WebexTeamsSender(Map config) { public AlertResult sendWebexTeamsAlter(AlertData alertData) { AlertResult alertResult = new AlertResult(); - alertResult.setStatus("false"); + alertResult.setSuccess(false); alertResult.setMessage("send webex teams alert fail."); try { @@ -93,7 +93,7 @@ private void send(AlertResult alertResult, AlertData alertData) throws IOExcepti String responseContent = EntityUtils.toString(entity, StandardCharsets.UTF_8); try { if (statusCode == HttpStatus.SC_OK) { - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage("send webex teams alert success"); } else { alertResult.setMessage(String.format( diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/test/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/test/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsSenderTest.java index 1d3070cb55bf..ddc806e593ec 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/test/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/test/java/org/apache/dolphinscheduler/plugin/alert/webexteams/WebexTeamsSenderTest.java @@ -85,6 +85,6 @@ public void testSendToPersonId() { public void testSend() { WebexTeamsSender webexTeamsSender = new WebexTeamsSender(webexTeamsConfig); AlertResult alertResult = webexTeamsSender.sendWebexTeamsAlter(alertData); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatAlertChannel.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatAlertChannel.java index 786cdb159f07..dcc53c7f59a2 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatAlertChannel.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatAlertChannel.java @@ -31,7 +31,7 @@ public AlertResult process(AlertInfo info) { AlertData alertData = info.getAlertData(); Map paramsMap = info.getAlertParams(); if (null == paramsMap) { - return new AlertResult("false", "we chat params is null"); + return new AlertResult(false, "we chat params is null"); } return new WeChatSender(paramsMap).sendEnterpriseWeChat(alertData.getTitle(), alertData.getContent()); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSender.java index c5ffec1f468c..d3fba217dcb0 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSender.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/main/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSender.java @@ -54,7 +54,6 @@ public final class WeChatSender { private static final String MUST_NOT_NULL = " must not null"; - private static final String ALERT_STATUS = "false"; private static final String AGENT_ID_REG_EXP = "{agentId}"; private static final String MSG_REG_EXP = "{msg}"; private static final String USER_REG_EXP = "{toUser}"; @@ -178,7 +177,7 @@ private static String mkString(Iterable list) { private static AlertResult checkWeChatSendMsgResult(String result) { AlertResult alertResult = new AlertResult(); - alertResult.setStatus(ALERT_STATUS); + alertResult.setSuccess(false); if (null == result) { alertResult.setMessage("we chat send fail"); @@ -192,11 +191,11 @@ private static AlertResult checkWeChatSendMsgResult(String result) { return alertResult; } if (sendMsgResponse.errcode == 0) { - alertResult.setStatus("true"); + alertResult.setSuccess(true); alertResult.setMessage("we chat alert send success"); return alertResult; } - alertResult.setStatus(ALERT_STATUS); + alertResult.setSuccess(false); alertResult.setMessage(sendMsgResponse.getErrmsg()); return alertResult; } @@ -212,7 +211,7 @@ public AlertResult sendEnterpriseWeChat(String title, String content) { if (null == weChatToken) { alertResult = new AlertResult(); alertResult.setMessage("send we chat alert fail,get weChat token error"); - alertResult.setStatus(ALERT_STATUS); + alertResult.setSuccess(false); return alertResult; } String enterpriseWeChatPushUrlReplace = ""; @@ -239,7 +238,7 @@ public AlertResult sendEnterpriseWeChat(String title, String content) { log.info("send we chat alert msg exception : {}", e.getMessage()); alertResult = new AlertResult(); alertResult.setMessage("send we chat alert fail"); - alertResult.setStatus(ALERT_STATUS); + alertResult.setSuccess(false); } return alertResult; } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/test/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSenderTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/test/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSenderTest.java index e0c934f436e9..6e4c318d131f 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/test/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSenderTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/test/java/org/apache/dolphinscheduler/plugin/alert/wechat/WeChatSenderTest.java @@ -71,7 +71,7 @@ public void testSendWeChatTableMsg() { WeChatSender weChatSender = new WeChatSender(weChatConfig); AlertResult alertResult = weChatSender.sendEnterpriseWeChat("test", content); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } @Test @@ -79,7 +79,7 @@ public void testSendWeChatTextMsg() { weChatConfig.put(AlertConstants.NAME_SHOW_TYPE, ShowType.TEXT.getDescp()); WeChatSender weChatSender = new WeChatSender(weChatConfig); AlertResult alertResult = weChatSender.sendEnterpriseWeChat("test", content); - Assertions.assertEquals("false", alertResult.getStatus()); + Assertions.assertFalse(alertResult.isSuccess()); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/pom.xml b/dolphinscheduler-alert/dolphinscheduler-alert-server/pom.xml index 507d7acb45c4..844a5983df8f 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/pom.xml +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/pom.xml @@ -66,6 +66,12 @@ org.springframework.cloud spring-cloud-starter-kubernetes-client-config + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java index 55c5c3446c99..24357110470a 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/AlertServer.java @@ -18,11 +18,7 @@ package org.apache.dolphinscheduler.alert; import org.apache.dolphinscheduler.alert.metrics.AlertServerMetrics; -import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager; -import org.apache.dolphinscheduler.alert.registry.AlertRegistryClient; -import org.apache.dolphinscheduler.alert.rpc.AlertRpcServer; import org.apache.dolphinscheduler.alert.service.AlertBootstrapService; -import org.apache.dolphinscheduler.alert.service.ListenerEventPostService; import org.apache.dolphinscheduler.common.CommonConfiguration; import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.lifecycle.ServerLifeCycleManager; @@ -50,14 +46,6 @@ public class AlertServer { @Autowired private AlertBootstrapService alertBootstrapService; - @Autowired - private ListenerEventPostService listenerEventPostService; - @Autowired - private AlertRpcServer alertRpcServer; - @Autowired - private AlertPluginManager alertPluginManager; - @Autowired - private AlertRegistryClient alertRegistryClient; public static void main(String[] args) { AlertServerMetrics.registerUncachedException(DefaultUncaughtExceptionHandler::getUncaughtExceptionCount); @@ -68,27 +56,14 @@ public static void main(String[] args) { @PostConstruct public void run() { - log.info("Alert server is staring ..."); - alertPluginManager.start(); - alertRegistryClient.start(); + log.info("AlertServer is staring ..."); alertBootstrapService.start(); - listenerEventPostService.start(); - alertRpcServer.start(); - log.info("Alert server is started ..."); + log.info("AlertServer is started ..."); } @PreDestroy public void close() { - destroy("alert server destroy"); - } - - /** - * gracefully stop - * - * @param cause stop cause - */ - public void destroy(String cause) { - + String cause = "AlertServer destroy"; try { // set stop signal is true // execute only once @@ -96,19 +71,14 @@ public void destroy(String cause) { log.warn("AlterServer is already stopped"); return; } - log.info("Alert server is stopping, cause: {}", cause); - try ( - AlertRpcServer closedAlertRpcServer = alertRpcServer; - AlertBootstrapService closedAlertBootstrapService = alertBootstrapService; - ListenerEventPostService closedListenerEventPostService = listenerEventPostService; - AlertRegistryClient closedAlertRegistryClient = alertRegistryClient) { - // close resource - } + log.info("AlertServer is stopping, cause: {}", cause); + alertBootstrapService.close(); // thread sleep 3 seconds for thread quietly stop ThreadUtils.sleep(Constants.SERVER_CLOSE_WAIT_TIME.toMillis()); - log.info("Alter server stopped, cause: {}", cause); + log.info("AlertServer stopped, cause: {}", cause); } catch (Exception e) { - log.error("Alert server stop failed, cause: {}", cause, e); + log.error("AlertServer stop failed, cause: {}", cause, e); } } + } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/config/AlertConfig.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/config/AlertConfig.java index 824851fd92a0..240f92b846f8 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/config/AlertConfig.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/config/AlertConfig.java @@ -43,6 +43,8 @@ public final class AlertConfig implements Validator { private Duration maxHeartbeatInterval = Duration.ofSeconds(60); + private int senderParallelism = 100; + private String alertServerAddress; @Override @@ -58,6 +60,10 @@ public void validate(Object target, Errors errors) { errors.rejectValue("max-heartbeat-interval", null, "should be a valid duration"); } + if (senderParallelism <= 0) { + errors.rejectValue("sender-parallelism", null, "should be a positive number"); + } + if (StringUtils.isEmpty(alertServerAddress)) { alertConfig.setAlertServerAddress(NetUtils.getAddr(alertConfig.getPort())); } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/metrics/AlertServerMetrics.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/metrics/AlertServerMetrics.java index db75a49371a5..606834a2e948 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/metrics/AlertServerMetrics.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/metrics/AlertServerMetrics.java @@ -45,6 +45,12 @@ public void registerPendingAlertGauge(final Supplier supplier) { .register(Metrics.globalRegistry); } + public void registerSendingAlertGauge(final Supplier supplier) { + Gauge.builder("ds.alert.sending", supplier) + .description("Number of sending alert") + .register(Metrics.globalRegistry); + } + public static void registerUncachedException(final Supplier supplier) { Gauge.builder("ds.alert.uncached.exception", supplier) .description("number of uncached exception") diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/plugin/AlertPluginManager.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/plugin/AlertPluginManager.java index 1035018e9cd8..badd463166af 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/plugin/AlertPluginManager.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/plugin/AlertPluginManager.java @@ -36,8 +36,8 @@ import org.springframework.stereotype.Component; -@Component @Slf4j +@Component public final class AlertPluginManager { private final PluginDao pluginDao; diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertHeartbeatTask.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertHeartbeatTask.java index 0bfefed223f3..a5481fdd492d 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertHeartbeatTask.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertHeartbeatTask.java @@ -18,6 +18,7 @@ package org.apache.dolphinscheduler.alert.registry; import org.apache.dolphinscheduler.alert.config.AlertConfig; +import org.apache.dolphinscheduler.alert.service.AlertHAServer; import org.apache.dolphinscheduler.common.enums.ServerStatus; import org.apache.dolphinscheduler.common.model.AlertServerHeartBeat; import org.apache.dolphinscheduler.common.model.BaseHeartBeatTask; @@ -42,12 +43,15 @@ public class AlertHeartbeatTask extends BaseHeartBeatTask private final RegistryClient registryClient; private final MetricsProvider metricsProvider; + + private final AlertHAServer alertHAServer; private final String heartBeatPath; private final long startupTime; public AlertHeartbeatTask(AlertConfig alertConfig, MetricsProvider metricsProvider, - RegistryClient registryClient) { + RegistryClient registryClient, + AlertHAServer alertHAServer) { super("AlertHeartbeatTask", alertConfig.getMaxHeartbeatInterval().toMillis()); this.startupTime = System.currentTimeMillis(); this.alertConfig = alertConfig; @@ -55,6 +59,7 @@ public AlertHeartbeatTask(AlertConfig alertConfig, this.registryClient = registryClient; this.heartBeatPath = RegistryNodeType.ALERT_SERVER.getRegistryPath() + "/" + alertConfig.getAlertServerAddress(); + this.alertHAServer = alertHAServer; this.processId = OSUtils.getProcessID(); } @@ -70,6 +75,7 @@ public AlertServerHeartBeat getHeartBeat() { .memoryUsage(systemMetrics.getSystemMemoryUsedPercentage()) .jvmMemoryUsage(systemMetrics.getJvmMemoryUsedPercentage()) .serverStatus(ServerStatus.NORMAL) + .isActive(alertHAServer.isActive()) .host(NetUtils.getHost()) .port(alertConfig.getPort()) .build(); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertRegistryClient.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertRegistryClient.java index 616220bd1bba..1b7839d81628 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertRegistryClient.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/registry/AlertRegistryClient.java @@ -18,9 +18,9 @@ package org.apache.dolphinscheduler.alert.registry; import org.apache.dolphinscheduler.alert.config.AlertConfig; +import org.apache.dolphinscheduler.alert.service.AlertHAServer; import org.apache.dolphinscheduler.meter.metrics.MetricsProvider; import org.apache.dolphinscheduler.registry.api.RegistryClient; -import org.apache.dolphinscheduler.registry.api.enums.RegistryNodeType; import lombok.extern.slf4j.Slf4j; @@ -42,10 +42,12 @@ public class AlertRegistryClient implements AutoCloseable { private AlertHeartbeatTask alertHeartbeatTask; + @Autowired + private AlertHAServer alertHAServer; + public void start() { log.info("AlertRegistryClient starting..."); - registryClient.getLock(RegistryNodeType.ALERT_LOCK.getRegistryPath()); - alertHeartbeatTask = new AlertHeartbeatTask(alertConfig, metricsProvider, registryClient); + alertHeartbeatTask = new AlertHeartbeatTask(alertConfig, metricsProvider, registryClient, alertHAServer); alertHeartbeatTask.start(); // start heartbeat task log.info("AlertRegistryClient started..."); @@ -55,7 +57,6 @@ public void start() { public void close() { log.info("AlertRegistryClient closing..."); alertHeartbeatTask.shutdown(); - registryClient.releaseLock(RegistryNodeType.ALERT_LOCK.getRegistryPath()); log.info("AlertRegistryClient closed..."); } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/rpc/AlertOperatorImpl.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/rpc/AlertOperatorImpl.java index 9f11fa6c2e5a..6a5ed3e0bec5 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/rpc/AlertOperatorImpl.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/rpc/AlertOperatorImpl.java @@ -16,7 +16,7 @@ */ package org.apache.dolphinscheduler.alert.rpc; -import org.apache.dolphinscheduler.alert.service.AlertBootstrapService; +import org.apache.dolphinscheduler.alert.service.AlertSender; import org.apache.dolphinscheduler.extract.alert.IAlertOperator; import org.apache.dolphinscheduler.extract.alert.request.AlertSendRequest; import org.apache.dolphinscheduler.extract.alert.request.AlertSendResponse; @@ -32,16 +32,15 @@ public class AlertOperatorImpl implements IAlertOperator { @Autowired - private AlertBootstrapService alertBootstrapService; + private AlertSender alertSender; @Override public AlertSendResponse sendAlert(AlertSendRequest alertSendRequest) { log.info("Received AlertSendRequest : {}", alertSendRequest); - AlertSendResponse alertSendResponse = alertBootstrapService.syncHandler( + AlertSendResponse alertSendResponse = alertSender.syncHandler( alertSendRequest.getGroupId(), alertSendRequest.getTitle(), - alertSendRequest.getContent(), - alertSendRequest.getWarnType()); + alertSendRequest.getContent()); log.info("Handle AlertSendRequest finish: {}", alertSendResponse); return alertSendResponse; } @@ -49,7 +48,7 @@ public AlertSendResponse sendAlert(AlertSendRequest alertSendRequest) { @Override public AlertSendResponse sendTestAlert(AlertTestSendRequest alertSendRequest) { log.info("Received AlertTestSendRequest : {}", alertSendRequest); - AlertSendResponse alertSendResponse = alertBootstrapService.syncTestSend( + AlertSendResponse alertSendResponse = alertSender.syncTestSend( alertSendRequest.getPluginDefineId(), alertSendRequest.getPluginInstanceParams()); log.info("Handle AlertTestSendRequest finish: {}", alertSendResponse); diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventFetcher.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventFetcher.java new file mode 100644 index 000000000000..1c61659ce38b --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventFetcher.java @@ -0,0 +1,100 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.common.thread.BaseDaemonThread; + +import org.apache.commons.collections4.CollectionUtils; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class AbstractEventFetcher extends BaseDaemonThread implements EventFetcher { + + protected static final int FETCH_SIZE = 100; + + protected static final long FETCH_INTERVAL = 5_000; + + protected final AlertHAServer alertHAServer; + + private final EventPendingQueue eventPendingQueue; + + private final AtomicBoolean runningFlag = new AtomicBoolean(false); + + private Integer eventOffset; + + protected AbstractEventFetcher(String fetcherName, + AlertHAServer alertHAServer, + EventPendingQueue eventPendingQueue) { + super(fetcherName); + this.alertHAServer = alertHAServer; + this.eventPendingQueue = eventPendingQueue; + this.eventOffset = -1; + } + + @Override + public synchronized void start() { + if (!runningFlag.compareAndSet(false, true)) { + throw new IllegalArgumentException("AlertEventFetcher is already started"); + } + log.info("AlertEventFetcher starting..."); + super.start(); + log.info("AlertEventFetcher started..."); + } + + @Override + public void run() { + while (runningFlag.get()) { + try { + if (!alertHAServer.isActive()) { + log.debug("The current node is not active, will not loop Alert"); + Thread.sleep(FETCH_INTERVAL); + continue; + } + List pendingEvents = fetchPendingEvent(eventOffset); + if (CollectionUtils.isEmpty(pendingEvents)) { + log.debug("No pending events found"); + Thread.sleep(FETCH_INTERVAL); + continue; + } + for (T alert : pendingEvents) { + eventPendingQueue.put(alert); + } + eventOffset = Math.max(eventOffset, + pendingEvents.stream().map(this::getEventOffset).max(Integer::compareTo).get()); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + } catch (Exception ex) { + log.error("AlertEventFetcher error", ex); + } + } + } + + protected abstract int getEventOffset(T event); + + @Override + public void shutdown() { + if (!runningFlag.compareAndSet(true, false)) { + log.warn("The AlertEventFetcher is not started"); + } + } + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventLoop.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventLoop.java new file mode 100644 index 000000000000..568125002e70 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventLoop.java @@ -0,0 +1,101 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.common.thread.BaseDaemonThread; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class AbstractEventLoop extends BaseDaemonThread implements EventLoop { + + private final EventPendingQueue eventPendingQueue; + + private final AtomicInteger handlingEventCount; + + private final int eventHandleWorkerNum; + + private final ThreadPoolExecutor threadPoolExecutor; + + private final AtomicBoolean runningFlag = new AtomicBoolean(false); + + protected AbstractEventLoop(String name, + ThreadPoolExecutor threadPoolExecutor, + EventPendingQueue eventPendingQueue) { + super(name); + this.handlingEventCount = new AtomicInteger(0); + this.eventHandleWorkerNum = threadPoolExecutor.getMaximumPoolSize(); + this.threadPoolExecutor = threadPoolExecutor; + this.eventPendingQueue = eventPendingQueue; + } + + @Override + public synchronized void start() { + if (!runningFlag.compareAndSet(false, true)) { + throw new IllegalArgumentException(getClass().getName() + " is already started"); + } + log.info("{} starting...", getClass().getName()); + super.start(); + log.info("{} started...", getClass().getName()); + } + + @Override + public void run() { + while (runningFlag.get()) { + try { + if (handlingEventCount.get() >= eventHandleWorkerNum) { + log.debug("There is no idle event worker, waiting for a while..."); + Thread.sleep(1000); + continue; + } + T pendingEvent = eventPendingQueue.take(); + handlingEventCount.incrementAndGet(); + CompletableFuture.runAsync(() -> handleEvent(pendingEvent), threadPoolExecutor) + .whenComplete((aVoid, throwable) -> { + if (throwable != null) { + log.error("Handle event: {} error", pendingEvent, throwable); + } + handlingEventCount.decrementAndGet(); + }); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + log.error("Loop event thread has been interrupted..."); + break; + } catch (Exception ex) { + log.error("Loop event error", ex); + } + } + } + + @Override + public int getHandlingEventCount() { + return handlingEventCount.get(); + } + + @Override + public void shutdown() { + if (!runningFlag.compareAndSet(true, false)) { + log.warn(getClass().getName() + " is not started"); + } + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventPendingQueue.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventPendingQueue.java new file mode 100644 index 000000000000..1d7e213ab95b --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventPendingQueue.java @@ -0,0 +1,53 @@ +/* + * 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.alert.service; + +import java.util.concurrent.LinkedBlockingQueue; + +public abstract class AbstractEventPendingQueue implements EventPendingQueue { + + private final LinkedBlockingQueue pendingAlertQueue; + + private final int capacity; + + protected AbstractEventPendingQueue(int capacity) { + this.capacity = capacity; + this.pendingAlertQueue = new LinkedBlockingQueue<>(capacity); + } + + @Override + public void put(T alert) throws InterruptedException { + pendingAlertQueue.put(alert); + } + + @Override + public T take() throws InterruptedException { + return pendingAlertQueue.take(); + } + + @Override + public int size() { + return pendingAlertQueue.size(); + } + + @Override + public int capacity() { + return capacity; + } + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventSender.java new file mode 100644 index 000000000000..deff97da49d7 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AbstractEventSender.java @@ -0,0 +1,191 @@ +/* + * 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.alert.service; + +import static com.google.common.base.Preconditions.checkNotNull; + +import org.apache.dolphinscheduler.alert.api.AlertChannel; +import org.apache.dolphinscheduler.alert.api.AlertConstants; +import org.apache.dolphinscheduler.alert.api.AlertData; +import org.apache.dolphinscheduler.alert.api.AlertInfo; +import org.apache.dolphinscheduler.alert.api.AlertResult; +import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager; +import org.apache.dolphinscheduler.common.enums.AlertStatus; +import org.apache.dolphinscheduler.common.enums.AlertType; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.dao.entity.AlertPluginInstance; +import org.apache.dolphinscheduler.dao.entity.AlertSendStatus; +import org.apache.dolphinscheduler.extract.alert.request.AlertSendResponse; +import org.apache.dolphinscheduler.spi.params.PluginParamsTransfer; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; + +@Slf4j +public abstract class AbstractEventSender implements EventSender { + + protected final AlertPluginManager alertPluginManager; + + private final long sendEventTimeout; + + protected AbstractEventSender(AlertPluginManager alertPluginManager, long sendEventTimeout) { + this.alertPluginManager = alertPluginManager; + this.sendEventTimeout = sendEventTimeout; + } + + @Override + public void sendEvent(T event) { + List alertPluginInstanceList = getAlertPluginInstanceList(event); + if (CollectionUtils.isEmpty(alertPluginInstanceList)) { + onError(event, "No bind plugin instance found"); + return; + } + AlertData alertData = getAlertData(event); + List alertSendStatuses = new ArrayList<>(); + for (AlertPluginInstance instance : alertPluginInstanceList) { + AlertResult alertResult = doSendEvent(instance, alertData); + AlertStatus alertStatus = + alertResult.isSuccess() ? AlertStatus.EXECUTION_SUCCESS : AlertStatus.EXECUTION_FAILURE; + AlertSendStatus alertSendStatus = AlertSendStatus.builder() + .alertId(getEventId(event)) + .alertPluginInstanceId(instance.getId()) + .sendStatus(alertStatus) + .log(JSONUtils.toJsonString(alertResult)) + .createTime(new Date()) + .build(); + alertSendStatuses.add(alertSendStatus); + } + long failureCount = alertSendStatuses.stream() + .map(alertSendStatus -> alertSendStatus.getSendStatus() == AlertStatus.EXECUTION_FAILURE) + .count(); + long successCount = alertSendStatuses.stream() + .map(alertSendStatus -> alertSendStatus.getSendStatus() == AlertStatus.EXECUTION_SUCCESS) + .count(); + if (successCount == 0) { + onError(event, JSONUtils.toJsonString(alertSendStatuses)); + } else { + if (failureCount > 0) { + onPartialSuccess(event, JSONUtils.toJsonString(alertSendStatuses)); + } else { + onSuccess(event, JSONUtils.toJsonString(alertSendStatuses)); + } + } + } + + public abstract List getAlertPluginInstanceList(T event); + + public abstract AlertData getAlertData(T event); + + public abstract Integer getEventId(T event); + + public abstract void onError(T event, String log); + + public abstract void onPartialSuccess(T event, String log); + + public abstract void onSuccess(T event, String log); + + @Override + public AlertResult doSendEvent(AlertPluginInstance instance, AlertData alertData) { + int pluginDefineId = instance.getPluginDefineId(); + Optional alertChannelOptional = alertPluginManager.getAlertChannel(pluginDefineId); + if (!alertChannelOptional.isPresent()) { + return AlertResult.fail("Cannot find the alertPlugin: " + pluginDefineId); + } + AlertChannel alertChannel = alertChannelOptional.get(); + + AlertInfo alertInfo = AlertInfo.builder() + .alertData(alertData) + .alertParams(PluginParamsTransfer.getPluginParamsMap(instance.getPluginInstanceParams())) + .alertPluginInstanceId(instance.getId()) + .build(); + try { + AlertResult alertResult; + if (sendEventTimeout <= 0) { + if (alertData.getAlertType() == AlertType.CLOSE_ALERT.getCode()) { + alertResult = alertChannel.closeAlert(alertInfo); + } else { + alertResult = alertChannel.process(alertInfo); + } + } else { + CompletableFuture future; + if (alertData.getAlertType() == AlertType.CLOSE_ALERT.getCode()) { + future = CompletableFuture.supplyAsync(() -> alertChannel.closeAlert(alertInfo)); + } else { + future = CompletableFuture.supplyAsync(() -> alertChannel.process(alertInfo)); + } + alertResult = future.get(sendEventTimeout, TimeUnit.MILLISECONDS); + } + checkNotNull(alertResult, "AlertResult cannot be null"); + return alertResult; + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + return AlertResult.fail(ExceptionUtils.getMessage(interruptedException)); + } catch (Exception e) { + log.error("Send alert data {} failed", alertData, e); + return AlertResult.fail(ExceptionUtils.getMessage(e)); + } + } + + @Override + public AlertSendResponse syncTestSend(int pluginDefineId, String pluginInstanceParams) { + + Optional alertChannelOptional = alertPluginManager.getAlertChannel(pluginDefineId); + if (!alertChannelOptional.isPresent()) { + AlertSendResponse.AlertSendResponseResult alertSendResponseResult = + AlertSendResponse.AlertSendResponseResult.fail("Cannot find the alertPlugin: " + pluginDefineId); + return AlertSendResponse.fail(Lists.newArrayList(alertSendResponseResult)); + } + AlertData alertData = AlertData.builder() + .title(AlertConstants.TEST_TITLE) + .content(AlertConstants.TEST_CONTENT) + .build(); + + AlertInfo alertInfo = AlertInfo.builder() + .alertData(alertData) + .alertParams(PluginParamsTransfer.getPluginParamsMap(pluginInstanceParams)) + .build(); + + try { + AlertResult alertResult = alertChannelOptional.get().process(alertInfo); + Preconditions.checkNotNull(alertResult, "AlertResult cannot be null"); + if (alertResult.isSuccess()) { + return AlertSendResponse + .success(Lists.newArrayList(AlertSendResponse.AlertSendResponseResult.success())); + } + return AlertSendResponse.fail( + Lists.newArrayList(AlertSendResponse.AlertSendResponseResult.fail(alertResult.getMessage()))); + } catch (Exception e) { + log.error("Test send alert error", e); + return new AlertSendResponse(false, + Lists.newArrayList(AlertSendResponse.AlertSendResponseResult.fail(ExceptionUtils.getMessage(e)))); + } + + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertBootstrapService.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertBootstrapService.java index 77e62a65a0b0..5553e01bc9b9 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertBootstrapService.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertBootstrapService.java @@ -17,350 +17,84 @@ package org.apache.dolphinscheduler.alert.service; -import org.apache.dolphinscheduler.alert.api.AlertChannel; -import org.apache.dolphinscheduler.alert.api.AlertConstants; -import org.apache.dolphinscheduler.alert.api.AlertData; -import org.apache.dolphinscheduler.alert.api.AlertInfo; -import org.apache.dolphinscheduler.alert.api.AlertResult; -import org.apache.dolphinscheduler.alert.config.AlertConfig; -import org.apache.dolphinscheduler.alert.metrics.AlertServerMetrics; import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager; -import org.apache.dolphinscheduler.common.constants.Constants; -import org.apache.dolphinscheduler.common.enums.AlertStatus; -import org.apache.dolphinscheduler.common.enums.AlertType; -import org.apache.dolphinscheduler.common.enums.WarningType; -import org.apache.dolphinscheduler.common.lifecycle.ServerLifeCycleManager; -import org.apache.dolphinscheduler.common.thread.BaseDaemonThread; -import org.apache.dolphinscheduler.common.thread.ThreadUtils; -import org.apache.dolphinscheduler.common.utils.JSONUtils; -import org.apache.dolphinscheduler.dao.AlertDao; -import org.apache.dolphinscheduler.dao.entity.Alert; -import org.apache.dolphinscheduler.dao.entity.AlertPluginInstance; -import org.apache.dolphinscheduler.dao.entity.AlertSendStatus; -import org.apache.dolphinscheduler.extract.alert.request.AlertSendResponse; -import org.apache.dolphinscheduler.spi.params.PluginParamsTransfer; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; +import org.apache.dolphinscheduler.alert.registry.AlertRegistryClient; +import org.apache.dolphinscheduler.alert.rpc.AlertRpcServer; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.google.common.collect.Lists; - -@Service +/** + * The bootstrap service for alert server. it will start all the necessary component for alert server. + */ @Slf4j -public final class AlertBootstrapService extends BaseDaemonThread implements AutoCloseable { - - @Autowired - private AlertDao alertDao; - @Autowired - private AlertPluginManager alertPluginManager; - @Autowired - private AlertConfig alertConfig; - - public AlertBootstrapService() { - super("AlertBootstrapService"); - } - - @Override - public void run() { - log.info("Alert sender thread started"); - while (!ServerLifeCycleManager.isStopped()) { - try { - List alerts = alertDao.listPendingAlerts(); - if (CollectionUtils.isEmpty(alerts)) { - log.debug("There is not waiting alerts"); - continue; - } - AlertServerMetrics.registerPendingAlertGauge(alerts::size); - this.send(alerts); - } catch (Exception e) { - log.error("Alert sender thread meet an exception", e); - } finally { - ThreadUtils.sleep(Constants.SLEEP_TIME_MILLIS * 5L); - } - } - log.info("Alert sender thread stopped"); - } - - public void send(List alerts) { - for (Alert alert : alerts) { - // get alert group from alert - int alertId = alert.getId(); - int alertGroupId = Optional.ofNullable(alert.getAlertGroupId()).orElse(0); - List alertInstanceList = alertDao.listInstanceByAlertGroupId(alertGroupId); - if (CollectionUtils.isEmpty(alertInstanceList)) { - log.error("send alert msg fail,no bind plugin instance."); - List alertResults = Lists.newArrayList(new AlertResult("false", - "no bind plugin instance")); - alertDao.updateAlert(AlertStatus.EXECUTION_FAILURE, JSONUtils.toJsonString(alertResults), alertId); - continue; - } - AlertData alertData = AlertData.builder() - .id(alertId) - .content(alert.getContent()) - .log(alert.getLog()) - .title(alert.getTitle()) - .warnType(alert.getWarningType().getCode()) - .alertType(alert.getAlertType().getCode()) - .build(); - - int sendSuccessCount = 0; - List alertSendStatuses = new ArrayList<>(); - List alertResults = new ArrayList<>(); - for (AlertPluginInstance instance : alertInstanceList) { - AlertResult alertResult = this.alertResultHandler(instance, alertData); - if (alertResult != null) { - AlertStatus sendStatus = Boolean.parseBoolean(alertResult.getStatus()) - ? AlertStatus.EXECUTION_SUCCESS - : AlertStatus.EXECUTION_FAILURE; - AlertSendStatus alertSendStatus = AlertSendStatus.builder() - .alertId(alertId) - .alertPluginInstanceId(instance.getId()) - .sendStatus(sendStatus) - .log(JSONUtils.toJsonString(alertResult)) - .createTime(new Date()) - .build(); - alertSendStatuses.add(alertSendStatus); - if (AlertStatus.EXECUTION_SUCCESS.equals(sendStatus)) { - sendSuccessCount++; - AlertServerMetrics.incAlertSuccessCount(); - } else { - AlertServerMetrics.incAlertFailCount(); - } - alertResults.add(alertResult); - } - } - AlertStatus alertStatus = AlertStatus.EXECUTION_SUCCESS; - if (sendSuccessCount == 0) { - alertStatus = AlertStatus.EXECUTION_FAILURE; - } else if (sendSuccessCount < alertInstanceList.size()) { - alertStatus = AlertStatus.EXECUTION_PARTIAL_SUCCESS; - } - // we update the alert first to avoid duplicate key in alertSendStatus - // this may loss the alertSendStatus if the server restart - // todo: use transaction to update these two table - alertDao.updateAlert(alertStatus, JSONUtils.toJsonString(alertResults), alertId); - alertDao.insertAlertSendStatus(alertSendStatuses); - } - } - - /** - * sync send alert handler - * - * @param alertGroupId alertGroupId - * @param title title - * @param content content - * @return AlertSendResponseCommand - */ - public AlertSendResponse syncHandler(int alertGroupId, String title, String content, int warnType) { - List alertInstanceList = alertDao.listInstanceByAlertGroupId(alertGroupId); - AlertData alertData = AlertData.builder() - .content(content) - .title(title) - .warnType(warnType) - .build(); - - boolean sendResponseStatus = true; - List sendResponseResults = new ArrayList<>(); - - if (CollectionUtils.isEmpty(alertInstanceList)) { - AlertSendResponse.AlertSendResponseResult alertSendResponseResult = - new AlertSendResponse.AlertSendResponseResult(); - String message = String.format("Alert GroupId %s send error : not found alert instance", alertGroupId); - alertSendResponseResult.setSuccess(false); - alertSendResponseResult.setMessage(message); - sendResponseResults.add(alertSendResponseResult); - log.error("Alert GroupId {} send error : not found alert instance", alertGroupId); - return new AlertSendResponse(false, sendResponseResults); - } - - for (AlertPluginInstance instance : alertInstanceList) { - AlertResult alertResult = this.alertResultHandler(instance, alertData); - if (alertResult != null) { - AlertSendResponse.AlertSendResponseResult alertSendResponseResult = - new AlertSendResponse.AlertSendResponseResult( - Boolean.parseBoolean(alertResult.getStatus()), - alertResult.getMessage()); - sendResponseStatus = sendResponseStatus && alertSendResponseResult.isSuccess(); - sendResponseResults.add(alertSendResponseResult); - } - } +@Service +public final class AlertBootstrapService implements AutoCloseable { - return new AlertSendResponse(sendResponseStatus, sendResponseResults); - } + private final AlertRpcServer alertRpcServer; - /** - * alert result handler - * - * @param instance instance - * @param alertData alertData - * @return AlertResult - */ - private @Nullable AlertResult alertResultHandler(AlertPluginInstance instance, AlertData alertData) { - String pluginInstanceName = instance.getInstanceName(); - int pluginDefineId = instance.getPluginDefineId(); - Optional alertChannelOptional = alertPluginManager.getAlertChannel(instance.getPluginDefineId()); - if (!alertChannelOptional.isPresent()) { - String message = String.format("Alert Plugin %s send error: the channel doesn't exist, pluginDefineId: %s", - pluginInstanceName, - pluginDefineId); - log.error("Alert Plugin {} send error : not found plugin {}", pluginInstanceName, pluginDefineId); - return new AlertResult("false", message); - } - AlertChannel alertChannel = alertChannelOptional.get(); + private final AlertRegistryClient alertRegistryClient; - Map paramsMap = JSONUtils.toMap(instance.getPluginInstanceParams()); - String instanceWarnType = WarningType.ALL.getDescp(); + private final AlertPluginManager alertPluginManager; - if (MapUtils.isNotEmpty(paramsMap)) { - instanceWarnType = paramsMap.getOrDefault(AlertConstants.NAME_WARNING_TYPE, WarningType.ALL.getDescp()); - } + private final AlertHAServer alertHAServer; - WarningType warningType = WarningType.of(instanceWarnType); + private final AlertEventFetcher alertEventFetcher; - if (warningType == null) { - String message = String.format("Alert Plugin %s send error : plugin warnType is null", pluginInstanceName); - log.error("Alert Plugin {} send error : plugin warnType is null", pluginInstanceName); - return new AlertResult("false", message); - } + private final AlertEventLoop alertEventLoop; - boolean sendWarning = false; - switch (warningType) { - case ALL: - sendWarning = true; - break; - case SUCCESS: - if (alertData.getWarnType() == WarningType.SUCCESS.getCode()) { - sendWarning = true; - } - break; - case FAILURE: - if (alertData.getWarnType() == WarningType.FAILURE.getCode()) { - sendWarning = true; - } - break; - default: - } + private final ListenerEventLoop listenerEventLoop; - if (!sendWarning) { - String message = String.format( - "Alert Plugin %s send ignore warning type not match: plugin warning type is %s, alert data warning type is %s", - pluginInstanceName, warningType.getCode(), alertData.getWarnType()); - log.info( - "Alert Plugin {} send ignore warning type not match: plugin warning type is {}, alert data warning type is {}", - pluginInstanceName, warningType.getCode(), alertData.getWarnType()); - return new AlertResult("false", message); - } + private final ListenerEventFetcher listenerEventFetcher; - AlertInfo alertInfo = AlertInfo.builder() - .alertData(alertData) - .alertParams(paramsMap) - .alertPluginInstanceId(instance.getId()) - .build(); - int waitTimeout = alertConfig.getWaitTimeout(); - try { - AlertResult alertResult; - if (waitTimeout <= 0) { - if (alertData.getAlertType() == AlertType.CLOSE_ALERT.getCode()) { - alertResult = alertChannel.closeAlert(alertInfo); - } else { - alertResult = alertChannel.process(alertInfo); - } - } else { - CompletableFuture future; - if (alertData.getAlertType() == AlertType.CLOSE_ALERT.getCode()) { - future = CompletableFuture.supplyAsync(() -> alertChannel.closeAlert(alertInfo)); - } else { - future = CompletableFuture.supplyAsync(() -> alertChannel.process(alertInfo)); - } - alertResult = future.get(waitTimeout, TimeUnit.MILLISECONDS); - } - if (alertResult == null) { - throw new RuntimeException("Alert result cannot be null"); - } - return alertResult; - } catch (InterruptedException e) { - log.error("send alert error alert data id :{},", alertData.getId(), e); - Thread.currentThread().interrupt(); - return new AlertResult("false", e.getMessage()); - } catch (Exception e) { - log.error("send alert error alert data id :{},", alertData.getId(), e); - return new AlertResult("false", e.getMessage()); - } + public AlertBootstrapService(AlertRpcServer alertRpcServer, + AlertRegistryClient alertRegistryClient, + AlertPluginManager alertPluginManager, + AlertHAServer alertHAServer, + AlertEventFetcher alertEventFetcher, + AlertEventLoop alertEventLoop, + ListenerEventLoop listenerEventLoop, + ListenerEventFetcher listenerEventFetcher) { + this.alertRpcServer = alertRpcServer; + this.alertRegistryClient = alertRegistryClient; + this.alertPluginManager = alertPluginManager; + this.alertHAServer = alertHAServer; + this.alertEventFetcher = alertEventFetcher; + this.alertEventLoop = alertEventLoop; + this.listenerEventLoop = listenerEventLoop; + this.listenerEventFetcher = listenerEventFetcher; } - public AlertSendResponse syncTestSend(int pluginDefineId, String pluginInstanceParams) { - - boolean sendResponseStatus = true; - List sendResponseResults = new ArrayList<>(); - - Optional alertChannelOptional = alertPluginManager.getAlertChannel(pluginDefineId); - if (!alertChannelOptional.isPresent()) { - String message = String.format("Test send alert error: the channel doesn't exist, pluginDefineId: %s", - pluginDefineId); - AlertSendResponse.AlertSendResponseResult alertSendResponseResult = - new AlertSendResponse.AlertSendResponseResult(); - alertSendResponseResult.setSuccess(false); - alertSendResponseResult.setMessage(message); - sendResponseResults.add(alertSendResponseResult); - log.error("Test send alert error : not found plugin {}", pluginDefineId); - return new AlertSendResponse(false, sendResponseResults); - } - AlertChannel alertChannel = alertChannelOptional.get(); - - Map paramsMap = PluginParamsTransfer.getPluginParamsMap(pluginInstanceParams); + public void start() { + log.info("AlertBootstrapService starting..."); + alertPluginManager.start(); + alertRpcServer.start(); + alertRegistryClient.start(); + alertHAServer.start(); - AlertData alertData = AlertData.builder() - .title(AlertConstants.TEST_TITLE) - .content(AlertConstants.TEST_CONTENT) - .warnType(WarningType.ALL.getCode()) - .build(); + listenerEventFetcher.start(); + alertEventFetcher.start(); - AlertInfo alertInfo = AlertInfo.builder() - .alertData(alertData) - .alertParams(paramsMap) - .build(); - - try { - AlertResult alertResult = alertChannel.process(alertInfo); - if (alertResult != null) { - AlertSendResponse.AlertSendResponseResult alertSendResponseResult = - new AlertSendResponse.AlertSendResponseResult( - Boolean.parseBoolean(alertResult.getStatus()), - alertResult.getMessage()); - sendResponseStatus = alertSendResponseResult.isSuccess(); - sendResponseResults.add(alertSendResponseResult); - } - } catch (Exception e) { - log.error("Test send alert error", e); - AlertSendResponse.AlertSendResponseResult alertSendResponseResult = - new AlertSendResponse.AlertSendResponseResult(); - alertSendResponseResult.setSuccess(false); - alertSendResponseResult.setMessage(e.getMessage()); - sendResponseResults.add(alertSendResponseResult); - return new AlertSendResponse(false, sendResponseResults); - } - - return new AlertSendResponse(sendResponseStatus, sendResponseResults); + listenerEventLoop.start(); + alertEventLoop.start(); + log.info("AlertBootstrapService started..."); } @Override public void close() { - log.info("Closed AlertBootstrapService..."); + log.info("AlertBootstrapService stopping..."); + try ( + AlertRpcServer closedAlertRpcServer = alertRpcServer; + AlertRegistryClient closedAlertRegistryClient = alertRegistryClient) { + // close resource + listenerEventFetcher.shutdown(); + alertEventFetcher.shutdown(); + + listenerEventLoop.shutdown(); + alertEventLoop.shutdown(); + alertHAServer.shutdown(); + } + log.info("AlertBootstrapService stopped..."); } - } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventFetcher.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventFetcher.java new file mode 100644 index 000000000000..11a668ae1ded --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventFetcher.java @@ -0,0 +1,51 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.dao.AlertDao; +import org.apache.dolphinscheduler.dao.entity.Alert; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class AlertEventFetcher extends AbstractEventFetcher { + + private final AlertDao alertDao; + + public AlertEventFetcher(AlertHAServer alertHAServer, + AlertDao alertDao, + AlertEventPendingQueue alertEventPendingQueue) { + super("AlertEventFetcher", alertHAServer, alertEventPendingQueue); + this.alertDao = alertDao; + } + + @Override + public List fetchPendingEvent(int eventOffset) { + return alertDao.listPendingAlerts(eventOffset); + } + + @Override + protected int getEventOffset(Alert event) { + return event.getId(); + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventLoop.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventLoop.java new file mode 100644 index 000000000000..e975f1ad51aa --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventLoop.java @@ -0,0 +1,46 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.alert.metrics.AlertServerMetrics; +import org.apache.dolphinscheduler.dao.entity.Alert; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class AlertEventLoop extends AbstractEventLoop { + + private final AlertSender alertSender; + + public AlertEventLoop(AlertEventPendingQueue alertEventPendingQueue, + AlertSenderThreadPoolFactory alertSenderThreadPoolFactory, + AlertSender alertSender) { + super("AlertEventLoop", alertSenderThreadPoolFactory.getThreadPool(), alertEventPendingQueue); + this.alertSender = alertSender; + AlertServerMetrics.registerPendingAlertGauge(this::getHandlingEventCount); + } + + @Override + public void handleEvent(Alert event) { + alertSender.sendEvent(event); + } + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventPendingQueue.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventPendingQueue.java new file mode 100644 index 000000000000..17fe7ccd0b73 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertEventPendingQueue.java @@ -0,0 +1,33 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.alert.config.AlertConfig; +import org.apache.dolphinscheduler.alert.metrics.AlertServerMetrics; +import org.apache.dolphinscheduler.dao.entity.Alert; + +import org.springframework.stereotype.Component; + +@Component +public class AlertEventPendingQueue extends AbstractEventPendingQueue { + + public AlertEventPendingQueue(AlertConfig alertConfig) { + super(alertConfig.getSenderParallelism() * 3 + 1); + AlertServerMetrics.registerPendingAlertGauge(this::size); + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertHAServer.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertHAServer.java new file mode 100644 index 000000000000..998bc655c4a9 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertHAServer.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.dolphinscheduler.alert.service; + +import org.apache.dolphinscheduler.registry.api.Registry; +import org.apache.dolphinscheduler.registry.api.enums.RegistryNodeType; +import org.apache.dolphinscheduler.registry.api.ha.AbstractHAServer; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class AlertHAServer extends AbstractHAServer { + + public AlertHAServer(Registry registry) { + super(registry, RegistryNodeType.ALERT_LOCK.getRegistryPath()); + } + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSender.java new file mode 100644 index 000000000000..9c9cd034bdb6 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSender.java @@ -0,0 +1,131 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.alert.api.AlertData; +import org.apache.dolphinscheduler.alert.api.AlertResult; +import org.apache.dolphinscheduler.alert.config.AlertConfig; +import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager; +import org.apache.dolphinscheduler.common.enums.AlertStatus; +import org.apache.dolphinscheduler.dao.AlertDao; +import org.apache.dolphinscheduler.dao.entity.Alert; +import org.apache.dolphinscheduler.dao.entity.AlertPluginInstance; +import org.apache.dolphinscheduler.extract.alert.request.AlertSendResponse; + +import org.apache.commons.collections4.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class AlertSender extends AbstractEventSender { + + private final AlertDao alertDao; + + public AlertSender(AlertDao alertDao, + AlertPluginManager alertPluginManager, + AlertConfig alertConfig) { + super(alertPluginManager, alertConfig.getWaitTimeout()); + this.alertDao = alertDao; + } + + /** + * sync send alert handler + * + * @param alertGroupId alertGroupId + * @param title title + * @param content content + * @return AlertSendResponseCommand + */ + public AlertSendResponse syncHandler(int alertGroupId, String title, String content) { + List alertInstanceList = alertDao.listInstanceByAlertGroupId(alertGroupId); + AlertData alertData = AlertData.builder() + .content(content) + .title(title) + .build(); + + boolean sendResponseStatus = true; + List sendResponseResults = new ArrayList<>(); + + if (CollectionUtils.isEmpty(alertInstanceList)) { + AlertSendResponse.AlertSendResponseResult alertSendResponseResult = + new AlertSendResponse.AlertSendResponseResult(); + String message = String.format("Alert GroupId %s send error : not found alert instance", alertGroupId); + alertSendResponseResult.setSuccess(false); + alertSendResponseResult.setMessage(message); + sendResponseResults.add(alertSendResponseResult); + log.error("Alert GroupId {} send error : not found alert instance", alertGroupId); + return new AlertSendResponse(false, sendResponseResults); + } + + for (AlertPluginInstance instance : alertInstanceList) { + AlertResult alertResult = doSendEvent(instance, alertData); + if (alertResult != null) { + AlertSendResponse.AlertSendResponseResult alertSendResponseResult = + new AlertSendResponse.AlertSendResponseResult( + alertResult.isSuccess(), + alertResult.getMessage()); + sendResponseStatus = sendResponseStatus && alertSendResponseResult.isSuccess(); + sendResponseResults.add(alertSendResponseResult); + } + } + + return new AlertSendResponse(sendResponseStatus, sendResponseResults); + } + + @Override + public List getAlertPluginInstanceList(Alert event) { + return alertDao.listInstanceByAlertGroupId(event.getAlertGroupId()); + } + + @Override + public AlertData getAlertData(Alert event) { + return AlertData.builder() + .id(event.getId()) + .content(event.getContent()) + .log(event.getLog()) + .title(event.getTitle()) + .alertType(event.getAlertType().getCode()) + .build(); + } + + @Override + public Integer getEventId(Alert event) { + return event.getId(); + } + + @Override + public void onError(Alert event, String log) { + alertDao.updateAlert(AlertStatus.EXECUTION_FAILURE, log, event.getId()); + } + + @Override + public void onPartialSuccess(Alert event, String log) { + alertDao.updateAlert(AlertStatus.EXECUTION_PARTIAL_SUCCESS, log, event.getId()); + } + + @Override + public void onSuccess(Alert event, String log) { + alertDao.updateAlert(AlertStatus.EXECUTION_SUCCESS, log, event.getId()); + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSenderThreadPoolFactory.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSenderThreadPoolFactory.java new file mode 100644 index 000000000000..fd8c731b1721 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/AlertSenderThreadPoolFactory.java @@ -0,0 +1,41 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.alert.config.AlertConfig; +import org.apache.dolphinscheduler.common.thread.ThreadUtils; + +import java.util.concurrent.ThreadPoolExecutor; + +import org.springframework.stereotype.Component; + +@Component +public class AlertSenderThreadPoolFactory { + + private final ThreadPoolExecutor threadPool; + + public AlertSenderThreadPoolFactory(AlertConfig alertConfig) { + this.threadPool = ThreadUtils.newDaemonFixedThreadExecutor("AlertSenderThread", + alertConfig.getSenderParallelism()); + } + + public ThreadPoolExecutor getThreadPool() { + return threadPool; + } + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventFetcher.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventFetcher.java new file mode 100644 index 000000000000..089fb4edc941 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventFetcher.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.dolphinscheduler.alert.service; + +import java.util.List; + +/** + * The interface responsible for fetching events. + * + * @param the type of event + */ +public interface EventFetcher { + + void start(); + + List fetchPendingEvent(int eventOffset); + + void shutdown(); +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventLoop.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventLoop.java new file mode 100644 index 000000000000..04219f99ae83 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventLoop.java @@ -0,0 +1,47 @@ +/* + * 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.alert.service; + +/** + * The interface responsible for consuming event from upstream, e.g {@link EventPendingQueue}. + * + * @param the type of event + */ +public interface EventLoop { + + /** + * Start the event loop, once the event loop is started, it will keep consuming event from upstream. + */ + void start(); + + /** + * Handle the given event. + */ + void handleEvent(T event); + + /** + * Get the count of handling event. + */ + int getHandlingEventCount(); + + /** + * Shutdown the event loop, once the event loop is shutdown, it will stop consuming event from upstream. + */ + void shutdown(); + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventPendingQueue.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventPendingQueue.java new file mode 100644 index 000000000000..c8538138bc92 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventPendingQueue.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.dolphinscheduler.alert.service; + +/** + * The interface responsible for managing pending events. + * + * @param the type of event + */ +public interface EventPendingQueue { + + void put(T alert) throws InterruptedException; + + T take() throws InterruptedException; + + int size(); + + int capacity(); +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventSender.java new file mode 100644 index 000000000000..04bc85e573e6 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/EventSender.java @@ -0,0 +1,33 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.alert.api.AlertData; +import org.apache.dolphinscheduler.alert.api.AlertResult; +import org.apache.dolphinscheduler.dao.entity.AlertPluginInstance; +import org.apache.dolphinscheduler.extract.alert.request.AlertSendResponse; + +public interface EventSender { + + void sendEvent(T event); + + AlertResult doSendEvent(AlertPluginInstance instance, AlertData alertData); + + AlertSendResponse syncTestSend(int pluginDefineId, String pluginInstanceParams); + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventFetcher.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventFetcher.java new file mode 100644 index 000000000000..57549d7e975b --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventFetcher.java @@ -0,0 +1,51 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.dao.entity.ListenerEvent; +import org.apache.dolphinscheduler.dao.repository.ListenerEventDao; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class ListenerEventFetcher extends AbstractEventFetcher { + + private final ListenerEventDao listenerEventDao; + + protected ListenerEventFetcher(AlertHAServer alertHAServer, + ListenerEventDao listenerEventDao, + ListenerEventPendingQueue listenerEventPendingQueue) { + super("ListenerEventFetcher", alertHAServer, listenerEventPendingQueue); + this.listenerEventDao = listenerEventDao; + } + + @Override + protected int getEventOffset(ListenerEvent event) { + return event.getId(); + } + + @Override + public List fetchPendingEvent(int eventOffset) { + return listenerEventDao.listingPendingEvents(eventOffset, FETCH_SIZE); + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventLoop.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventLoop.java new file mode 100644 index 000000000000..f1c00967f996 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventLoop.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.dolphinscheduler.alert.service; + +import org.apache.dolphinscheduler.dao.entity.ListenerEvent; + +import org.springframework.stereotype.Component; + +@Component +public class ListenerEventLoop extends AbstractEventLoop { + + private final ListenerEventSender listenerEventSender; + + protected ListenerEventLoop(AlertSenderThreadPoolFactory alertSenderThreadPoolFactory, + ListenerEventSender listenerEventSender, + ListenerEventPendingQueue listenerEventPendingQueue) { + super("ListenerEventLoop", alertSenderThreadPoolFactory.getThreadPool(), listenerEventPendingQueue); + this.listenerEventSender = listenerEventSender; + } + + @Override + public void handleEvent(ListenerEvent event) { + listenerEventSender.sendEvent(event); + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventPendingQueue.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventPendingQueue.java new file mode 100644 index 000000000000..47d0c77dff7e --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventPendingQueue.java @@ -0,0 +1,32 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.alert.config.AlertConfig; +import org.apache.dolphinscheduler.dao.entity.ListenerEvent; + +import org.springframework.stereotype.Component; + +@Component +public class ListenerEventPendingQueue extends AbstractEventPendingQueue { + + public ListenerEventPendingQueue(AlertConfig alertConfig) { + super(alertConfig.getSenderParallelism() * 3 + 1); + } + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventPostService.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventPostService.java deleted file mode 100644 index b57562c71101..000000000000 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventPostService.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * 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.alert.service; - -import org.apache.dolphinscheduler.alert.api.AlertChannel; -import org.apache.dolphinscheduler.alert.api.AlertData; -import org.apache.dolphinscheduler.alert.api.AlertInfo; -import org.apache.dolphinscheduler.alert.api.AlertResult; -import org.apache.dolphinscheduler.alert.config.AlertConfig; -import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager; -import org.apache.dolphinscheduler.common.constants.Constants; -import org.apache.dolphinscheduler.common.enums.AlertStatus; -import org.apache.dolphinscheduler.common.enums.AlertType; -import org.apache.dolphinscheduler.common.enums.WarningType; -import org.apache.dolphinscheduler.common.lifecycle.ServerLifeCycleManager; -import org.apache.dolphinscheduler.common.thread.BaseDaemonThread; -import org.apache.dolphinscheduler.common.thread.ThreadUtils; -import org.apache.dolphinscheduler.common.utils.JSONUtils; -import org.apache.dolphinscheduler.dao.entity.AlertPluginInstance; -import org.apache.dolphinscheduler.dao.entity.AlertSendStatus; -import org.apache.dolphinscheduler.dao.entity.ListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.AbstractListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.ProcessDefinitionCreatedListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.ProcessDefinitionDeletedListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.ProcessDefinitionUpdatedListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.ProcessEndListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.ProcessFailListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.ProcessStartListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.ServerDownListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.TaskEndListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.TaskFailListenerEvent; -import org.apache.dolphinscheduler.dao.entity.event.TaskStartListenerEvent; -import org.apache.dolphinscheduler.dao.mapper.AlertPluginInstanceMapper; -import org.apache.dolphinscheduler.dao.mapper.ListenerEventMapper; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.curator.shaded.com.google.common.collect.Lists; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - -import lombok.extern.slf4j.Slf4j; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Service -@Slf4j -public final class ListenerEventPostService extends BaseDaemonThread implements AutoCloseable { - - @Value("${alert.query_alert_threshold:100}") - private Integer QUERY_ALERT_THRESHOLD; - @Autowired - private ListenerEventMapper listenerEventMapper; - @Autowired - private AlertPluginInstanceMapper alertPluginInstanceMapper; - @Autowired - private AlertPluginManager alertPluginManager; - @Autowired - private AlertConfig alertConfig; - - public ListenerEventPostService() { - super("ListenerEventPostService"); - } - - @Override - public void run() { - log.info("listener event post thread started"); - while (!ServerLifeCycleManager.isStopped()) { - try { - List listenerEvents = listenerEventMapper - .listingListenerEventByStatus(AlertStatus.WAIT_EXECUTION, QUERY_ALERT_THRESHOLD); - if (CollectionUtils.isEmpty(listenerEvents)) { - log.debug("There is no waiting listener events"); - continue; - } - this.send(listenerEvents); - } catch (Exception e) { - log.error("listener event post thread meet an exception", e); - } finally { - ThreadUtils.sleep(Constants.SLEEP_TIME_MILLIS * 5L); - } - } - log.info("listener event post thread stopped"); - } - - public void send(List listenerEvents) { - for (ListenerEvent listenerEvent : listenerEvents) { - int eventId = listenerEvent.getId(); - List globalAlertInstanceList = - alertPluginInstanceMapper.queryAllGlobalAlertPluginInstanceList(); - if (CollectionUtils.isEmpty(globalAlertInstanceList)) { - log.error("post listener event fail,no bind global plugin instance."); - listenerEventMapper.updateListenerEvent(eventId, AlertStatus.EXECUTION_FAILURE, - "no bind plugin instance", new Date()); - continue; - } - AbstractListenerEvent event = generateEventFromContent(listenerEvent); - if (event == null) { - log.error("parse listener event to abstract listener event fail.ed {}", listenerEvent.getContent()); - listenerEventMapper.updateListenerEvent(eventId, AlertStatus.EXECUTION_FAILURE, - "parse listener event to abstract listener event failed", new Date()); - continue; - } - List events = Lists.newArrayList(event); - AlertData alertData = AlertData.builder() - .id(eventId) - .content(JSONUtils.toJsonString(events)) - .log(listenerEvent.getLog()) - .title(event.getTitle()) - .warnType(WarningType.GLOBAL.getCode()) - .alertType(event.getEventType().getCode()) - .build(); - - int sendSuccessCount = 0; - List failedPostResults = new ArrayList<>(); - for (AlertPluginInstance instance : globalAlertInstanceList) { - AlertResult alertResult = this.alertResultHandler(instance, alertData); - if (alertResult != null) { - AlertStatus sendStatus = Boolean.parseBoolean(alertResult.getStatus()) - ? AlertStatus.EXECUTION_SUCCESS - : AlertStatus.EXECUTION_FAILURE; - if (AlertStatus.EXECUTION_SUCCESS.equals(sendStatus)) { - sendSuccessCount++; - } else { - AlertSendStatus alertSendStatus = AlertSendStatus.builder() - .alertId(eventId) - .alertPluginInstanceId(instance.getId()) - .sendStatus(sendStatus) - .log(JSONUtils.toJsonString(alertResult)) - .createTime(new Date()) - .build(); - failedPostResults.add(alertSendStatus); - } - } - } - if (sendSuccessCount == globalAlertInstanceList.size()) { - listenerEventMapper.deleteById(eventId); - } else { - AlertStatus alertStatus = - sendSuccessCount == 0 ? AlertStatus.EXECUTION_FAILURE : AlertStatus.EXECUTION_PARTIAL_SUCCESS; - listenerEventMapper.updateListenerEvent(eventId, alertStatus, JSONUtils.toJsonString(failedPostResults), - new Date()); - } - } - } - - /** - * alert result handler - * - * @param instance instance - * @param alertData alertData - * @return AlertResult - */ - private @Nullable AlertResult alertResultHandler(AlertPluginInstance instance, AlertData alertData) { - String pluginInstanceName = instance.getInstanceName(); - int pluginDefineId = instance.getPluginDefineId(); - Optional alertChannelOptional = alertPluginManager.getAlertChannel(instance.getPluginDefineId()); - if (!alertChannelOptional.isPresent()) { - String message = - String.format("Global Alert Plugin %s send error: the channel doesn't exist, pluginDefineId: %s", - pluginInstanceName, - pluginDefineId); - log.error("Global Alert Plugin {} send error : not found plugin {}", pluginInstanceName, pluginDefineId); - return new AlertResult("false", message); - } - AlertChannel alertChannel = alertChannelOptional.get(); - - Map paramsMap = JSONUtils.toMap(instance.getPluginInstanceParams()); - - AlertInfo alertInfo = AlertInfo.builder() - .alertData(alertData) - .alertParams(paramsMap) - .alertPluginInstanceId(instance.getId()) - .build(); - int waitTimeout = alertConfig.getWaitTimeout(); - try { - AlertResult alertResult; - if (waitTimeout <= 0) { - if (alertData.getAlertType() == AlertType.CLOSE_ALERT.getCode()) { - alertResult = alertChannel.closeAlert(alertInfo); - } else { - alertResult = alertChannel.process(alertInfo); - } - } else { - CompletableFuture future; - if (alertData.getAlertType() == AlertType.CLOSE_ALERT.getCode()) { - future = CompletableFuture.supplyAsync(() -> alertChannel.closeAlert(alertInfo)); - } else { - future = CompletableFuture.supplyAsync(() -> alertChannel.process(alertInfo)); - } - alertResult = future.get(waitTimeout, TimeUnit.MILLISECONDS); - } - if (alertResult == null) { - throw new RuntimeException("Alert result cannot be null"); - } - return alertResult; - } catch (InterruptedException e) { - log.error("post listener event error alert data id :{},", alertData.getId(), e); - Thread.currentThread().interrupt(); - return new AlertResult("false", e.getMessage()); - } catch (Exception e) { - log.error("post listener event error alert data id :{},", alertData.getId(), e); - return new AlertResult("false", e.getMessage()); - } - } - - private AbstractListenerEvent generateEventFromContent(ListenerEvent listenerEvent) { - String content = listenerEvent.getContent(); - switch (listenerEvent.getEventType()) { - case SERVER_DOWN: - return JSONUtils.parseObject(content, ServerDownListenerEvent.class); - case PROCESS_DEFINITION_CREATED: - return JSONUtils.parseObject(content, ProcessDefinitionCreatedListenerEvent.class); - case PROCESS_DEFINITION_UPDATED: - return JSONUtils.parseObject(content, ProcessDefinitionUpdatedListenerEvent.class); - case PROCESS_DEFINITION_DELETED: - return JSONUtils.parseObject(content, ProcessDefinitionDeletedListenerEvent.class); - case PROCESS_START: - return JSONUtils.parseObject(content, ProcessStartListenerEvent.class); - case PROCESS_END: - return JSONUtils.parseObject(content, ProcessEndListenerEvent.class); - case PROCESS_FAIL: - return JSONUtils.parseObject(content, ProcessFailListenerEvent.class); - case TASK_START: - return JSONUtils.parseObject(content, TaskStartListenerEvent.class); - case TASK_END: - return JSONUtils.parseObject(content, TaskEndListenerEvent.class); - case TASK_FAIL: - return JSONUtils.parseObject(content, TaskFailListenerEvent.class); - default: - return null; - } - } - @Override - public void close() { - log.info("Closed ListenerEventPostService..."); - } -} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventSender.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventSender.java new file mode 100644 index 000000000000..7d06500edd2c --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/java/org/apache/dolphinscheduler/alert/service/ListenerEventSender.java @@ -0,0 +1,146 @@ +/* + * 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.alert.service; + +import org.apache.dolphinscheduler.alert.api.AlertData; +import org.apache.dolphinscheduler.alert.config.AlertConfig; +import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager; +import org.apache.dolphinscheduler.common.enums.AlertStatus; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.dao.entity.AlertPluginInstance; +import org.apache.dolphinscheduler.dao.entity.ListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.AbstractListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.ProcessDefinitionCreatedListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.ProcessDefinitionDeletedListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.ProcessDefinitionUpdatedListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.ProcessEndListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.ProcessFailListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.ProcessStartListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.ServerDownListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.TaskEndListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.TaskFailListenerEvent; +import org.apache.dolphinscheduler.dao.entity.event.TaskStartListenerEvent; +import org.apache.dolphinscheduler.dao.mapper.AlertPluginInstanceMapper; +import org.apache.dolphinscheduler.dao.repository.ListenerEventDao; + +import org.apache.curator.shaded.com.google.common.collect.Lists; + +import java.util.Date; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class ListenerEventSender extends AbstractEventSender { + + private final ListenerEventDao listenerEventDao; + + private final AlertPluginInstanceMapper alertPluginInstanceMapper; + + public ListenerEventSender(ListenerEventDao listenerEventDao, + AlertPluginInstanceMapper alertPluginInstanceMapper, + AlertPluginManager alertPluginManager, + AlertConfig alertConfig) { + super(alertPluginManager, alertConfig.getWaitTimeout()); + this.listenerEventDao = listenerEventDao; + this.alertPluginInstanceMapper = alertPluginInstanceMapper; + } + + private AbstractListenerEvent generateEventFromContent(ListenerEvent listenerEvent) { + String content = listenerEvent.getContent(); + AbstractListenerEvent event = null; + switch (listenerEvent.getEventType()) { + case SERVER_DOWN: + event = JSONUtils.parseObject(content, ServerDownListenerEvent.class); + break; + case PROCESS_DEFINITION_CREATED: + event = JSONUtils.parseObject(content, ProcessDefinitionCreatedListenerEvent.class); + break; + case PROCESS_DEFINITION_UPDATED: + event = JSONUtils.parseObject(content, ProcessDefinitionUpdatedListenerEvent.class); + break; + case PROCESS_DEFINITION_DELETED: + event = JSONUtils.parseObject(content, ProcessDefinitionDeletedListenerEvent.class); + break; + case PROCESS_START: + event = JSONUtils.parseObject(content, ProcessStartListenerEvent.class); + break; + case PROCESS_END: + event = JSONUtils.parseObject(content, ProcessEndListenerEvent.class); + break; + case PROCESS_FAIL: + event = JSONUtils.parseObject(content, ProcessFailListenerEvent.class); + break; + case TASK_START: + event = JSONUtils.parseObject(content, TaskStartListenerEvent.class); + break; + case TASK_END: + event = JSONUtils.parseObject(content, TaskEndListenerEvent.class); + break; + case TASK_FAIL: + event = JSONUtils.parseObject(content, TaskFailListenerEvent.class); + break; + default: + throw new IllegalArgumentException("Unsupported event type: " + listenerEvent.getEventType()); + } + if (event == null) { + throw new IllegalArgumentException("Failed to parse event from content: " + content); + } + return event; + } + + @Override + public List getAlertPluginInstanceList(ListenerEvent event) { + return alertPluginInstanceMapper.queryAllGlobalAlertPluginInstanceList(); + } + + @Override + public AlertData getAlertData(ListenerEvent listenerEvent) { + AbstractListenerEvent event = generateEventFromContent(listenerEvent); + return AlertData.builder() + .id(listenerEvent.getId()) + .content(JSONUtils.toJsonString(Lists.newArrayList(event))) + .log(listenerEvent.getLog()) + .title(event.getTitle()) + .alertType(event.getEventType().getCode()) + .build(); + } + + @Override + public Integer getEventId(ListenerEvent event) { + return event.getId(); + } + + @Override + public void onError(ListenerEvent event, String log) { + listenerEventDao.updateListenerEvent(event.getId(), AlertStatus.EXECUTION_FAILURE, log, new Date()); + } + + @Override + public void onPartialSuccess(ListenerEvent event, String log) { + listenerEventDao.updateListenerEvent(event.getId(), AlertStatus.EXECUTION_PARTIAL_SUCCESS, log, new Date()); + } + + @Override + public void onSuccess(ListenerEvent event, String log) { + listenerEventDao.updateListenerEvent(event.getId(), AlertStatus.EXECUTION_FAILURE, log, new Date()); + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/resources/application.yaml b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/resources/application.yaml index 0dbb6988ce28..6fbcc04febd6 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/resources/application.yaml +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/main/resources/application.yaml @@ -73,7 +73,8 @@ alert: # Define value is (0 = infinite), and alert server would be waiting alert result. wait-timeout: 0 max-heartbeat-interval: 60s - query_alert_threshold: 100 + # The maximum number of alerts that can be processed in parallel + sender-parallelism: 100 registry: type: zookeeper diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/config/AlertConfigTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/config/AlertConfigTest.java new file mode 100644 index 000000000000..1a72f0a5c93d --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/config/AlertConfigTest.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.dolphinscheduler.alert.config; + +import static com.google.common.truth.Truth.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; + +@AutoConfigureMockMvc +@SpringBootTest(classes = AlertConfig.class) +class AlertConfigTest { + + @Autowired + private AlertConfig alertConfig; + + @Test + void testValidate() { + assertThat(alertConfig.getWaitTimeout()).isEqualTo(10); + assertThat(alertConfig.getMaxHeartbeatInterval()).isEqualTo(Duration.ofSeconds(59)); + assertThat(alertConfig.getSenderParallelism()).isEqualTo(101); + } + +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertBootstrapServiceTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertSenderTest.java similarity index 73% rename from dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertBootstrapServiceTest.java rename to dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertSenderTest.java index eafba16585fe..400afd34dcbf 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertBootstrapServiceTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/AlertSenderTest.java @@ -24,15 +24,13 @@ import org.apache.dolphinscheduler.alert.api.AlertResult; import org.apache.dolphinscheduler.alert.config.AlertConfig; import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager; -import org.apache.dolphinscheduler.alert.service.AlertBootstrapService; +import org.apache.dolphinscheduler.alert.service.AlertSender; import org.apache.dolphinscheduler.common.enums.WarningType; import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.dolphinscheduler.dao.AlertDao; import org.apache.dolphinscheduler.dao.PluginDao; import org.apache.dolphinscheduler.dao.entity.Alert; import org.apache.dolphinscheduler.dao.entity.AlertPluginInstance; -import org.apache.dolphinscheduler.dao.entity.ListenerEvent; -import org.apache.dolphinscheduler.dao.entity.PluginDefine; import org.apache.dolphinscheduler.extract.alert.request.AlertSendResponse; import org.apache.dolphinscheduler.spi.params.PluginParamsTransfer; @@ -42,19 +40,20 @@ import java.util.Optional; 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.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class AlertBootstrapServiceTest { +@ExtendWith(MockitoExtension.class) +class AlertSenderTest { - private static final Logger logger = LoggerFactory.getLogger(AlertBootstrapServiceTest.class); + private static final Logger logger = LoggerFactory.getLogger(AlertSenderTest.class); @Mock private AlertDao alertDao; @@ -66,7 +65,7 @@ public class AlertBootstrapServiceTest { private AlertConfig alertConfig; @InjectMocks - private AlertBootstrapService alertBootstrapService; + private AlertSender alertSender; private static final String PLUGIN_INSTANCE_PARAMS = "{\"User\":\"xx\",\"receivers\":\"xx\",\"sender\":\"xx\",\"smtpSslTrust\":\"*\",\"enableSmtpAuth\":\"true\",\"receiverCcs\":null,\"showType\":\"table\",\"starttlsEnable\":\"false\",\"serverPort\":\"25\",\"serverHost\":\"xx\",\"Password\":\"xx\",\"sslEnable\":\"false\"}"; @@ -74,25 +73,17 @@ public class AlertBootstrapServiceTest { private static final String PLUGIN_INSTANCE_NAME = "alert-instance-mail"; private static final String TITLE = "alert mail test TITLE"; private static final String CONTENT = "alert mail test CONTENT"; - private static final List EVENTS = new ArrayList<>(); private static final int PLUGIN_DEFINE_ID = 1; private static final int ALERT_GROUP_ID = 1; - @BeforeEach - public void before() { - MockitoAnnotations.initMocks(this); - } - @Test - public void testSyncHandler() { + void testSyncHandler() { // 1.alert instance does not exist when(alertDao.listInstanceByAlertGroupId(ALERT_GROUP_ID)).thenReturn(null); - when(alertConfig.getWaitTimeout()).thenReturn(0); - AlertSendResponse alertSendResponse = - alertBootstrapService.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT, WarningType.ALL.getCode()); + AlertSendResponse alertSendResponse = alertSender.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT); Assertions.assertFalse(alertSendResponse.isSuccess()); alertSendResponse.getResResults().forEach(result -> logger .info("alert send response result, status:{}, message:{}", result.isSuccess(), result.getMessage())); @@ -108,12 +99,7 @@ public void testSyncHandler() { alertInstanceList.add(alertPluginInstance); when(alertDao.listInstanceByAlertGroupId(1)).thenReturn(alertInstanceList); - String pluginName = "alert-plugin-mail"; - PluginDefine pluginDefine = new PluginDefine(pluginName, "1", null); - when(pluginDao.getPluginDefineById(pluginDefineId)).thenReturn(pluginDefine); - - alertSendResponse = - alertBootstrapService.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT, WarningType.ALL.getCode()); + alertSendResponse = alertSender.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT); Assertions.assertFalse(alertSendResponse.isSuccess()); alertSendResponse.getResResults().forEach(result -> logger .info("alert send response result, status:{}, message:{}", result.isSuccess(), result.getMessage())); @@ -122,37 +108,32 @@ public void testSyncHandler() { AlertChannel alertChannelMock = mock(AlertChannel.class); when(alertChannelMock.process(Mockito.any())).thenReturn(null); when(alertPluginManager.getAlertChannel(1)).thenReturn(Optional.of(alertChannelMock)); - when(alertConfig.getWaitTimeout()).thenReturn(0); - alertSendResponse = - alertBootstrapService.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT, WarningType.ALL.getCode()); + alertSendResponse = alertSender.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT); Assertions.assertFalse(alertSendResponse.isSuccess()); alertSendResponse.getResResults().forEach(result -> logger .info("alert send response result, status:{}, message:{}", result.isSuccess(), result.getMessage())); // 4.abnormal information inside the alert plug-in code AlertResult alertResult = new AlertResult(); - alertResult.setStatus(String.valueOf(false)); + alertResult.setSuccess(false); alertResult.setMessage("Abnormal information inside the alert plug-in code"); when(alertChannelMock.process(Mockito.any())).thenReturn(alertResult); when(alertPluginManager.getAlertChannel(1)).thenReturn(Optional.of(alertChannelMock)); - alertSendResponse = - alertBootstrapService.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT, WarningType.ALL.getCode()); + alertSendResponse = alertSender.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT); Assertions.assertFalse(alertSendResponse.isSuccess()); alertSendResponse.getResResults().forEach(result -> logger .info("alert send response result, status:{}, message:{}", result.isSuccess(), result.getMessage())); // 5.alert plugin send success alertResult = new AlertResult(); - alertResult.setStatus(String.valueOf(true)); + alertResult.setSuccess(true); alertResult.setMessage(String.format("Alert Plugin %s send success", pluginInstanceName)); when(alertChannelMock.process(Mockito.any())).thenReturn(alertResult); when(alertPluginManager.getAlertChannel(1)).thenReturn(Optional.of(alertChannelMock)); - when(alertConfig.getWaitTimeout()).thenReturn(5000); - alertSendResponse = - alertBootstrapService.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT, WarningType.ALL.getCode()); + alertSendResponse = alertSender.syncHandler(ALERT_GROUP_ID, TITLE, CONTENT); Assertions.assertTrue(alertSendResponse.isSuccess()); alertSendResponse.getResResults().forEach(result -> logger .info("alert send response result, status:{}, message:{}", result.isSuccess(), result.getMessage())); @@ -160,17 +141,13 @@ public void testSyncHandler() { } @Test - public void testRun() { - List alertList = new ArrayList<>(); + void testRun() { Alert alert = new Alert(); alert.setId(1); alert.setAlertGroupId(ALERT_GROUP_ID); alert.setTitle(TITLE); alert.setContent(CONTENT); alert.setWarningType(WarningType.FAILURE); - alertList.add(alert); - - // alertSenderService = new AlertSenderService(); int pluginDefineId = 1; String pluginInstanceParams = "alert-instance-mail-params"; @@ -181,25 +158,18 @@ public void testRun() { alertInstanceList.add(alertPluginInstance); when(alertDao.listInstanceByAlertGroupId(ALERT_GROUP_ID)).thenReturn(alertInstanceList); - String pluginName = "alert-plugin-mail"; - PluginDefine pluginDefine = new PluginDefine(pluginName, "1", null); - when(pluginDao.getPluginDefineById(pluginDefineId)).thenReturn(pluginDefine); - AlertResult alertResult = new AlertResult(); - alertResult.setStatus(String.valueOf(true)); + alertResult.setSuccess(true); alertResult.setMessage(String.format("Alert Plugin %s send success", pluginInstanceName)); - AlertChannel alertChannelMock = mock(AlertChannel.class); - when(alertChannelMock.process(Mockito.any())).thenReturn(alertResult); - when(alertPluginManager.getAlertChannel(1)).thenReturn(Optional.of(alertChannelMock)); - Assertions.assertTrue(Boolean.parseBoolean(alertResult.getStatus())); + Assertions.assertTrue(alertResult.isSuccess()); when(alertDao.listInstanceByAlertGroupId(1)).thenReturn(new ArrayList<>()); - alertBootstrapService.send(alertList); + alertSender.sendEvent(alert); } @Test - public void testSendAlert() { + void testSendAlert() { AlertResult sendResult = new AlertResult(); - sendResult.setStatus(String.valueOf(true)); + sendResult.setSuccess(true); sendResult.setMessage(String.format("Alert Plugin %s send success", PLUGIN_INSTANCE_NAME)); AlertChannel alertChannelMock = mock(AlertChannel.class); when(alertChannelMock.process(Mockito.any())).thenReturn(sendResult); @@ -209,6 +179,6 @@ public void testSendAlert() { Mockito.mockStatic(PluginParamsTransfer.class); pluginParamsTransferMockedStatic.when(() -> PluginParamsTransfer.getPluginParamsMap(PLUGIN_INSTANCE_PARAMS)) .thenReturn(paramsMap); - alertBootstrapService.syncTestSend(PLUGIN_DEFINE_ID, PLUGIN_INSTANCE_PARAMS); + alertSender.syncTestSend(PLUGIN_DEFINE_ID, PLUGIN_INSTANCE_PARAMS); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/ListenerEventPostServiceTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/ListenerEventSenderTest.java similarity index 82% rename from dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/ListenerEventPostServiceTest.java rename to dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/ListenerEventSenderTest.java index 33917267f0e6..0304be022c22 100644 --- a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/ListenerEventPostServiceTest.java +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/runner/ListenerEventSenderTest.java @@ -24,7 +24,7 @@ import org.apache.dolphinscheduler.alert.api.AlertResult; import org.apache.dolphinscheduler.alert.config.AlertConfig; import org.apache.dolphinscheduler.alert.plugin.AlertPluginManager; -import org.apache.dolphinscheduler.alert.service.ListenerEventPostService; +import org.apache.dolphinscheduler.alert.service.ListenerEventSender; import org.apache.dolphinscheduler.common.enums.AlertPluginInstanceType; import org.apache.dolphinscheduler.common.enums.AlertStatus; import org.apache.dolphinscheduler.common.enums.ListenerEventType; @@ -33,7 +33,7 @@ import org.apache.dolphinscheduler.dao.entity.ListenerEvent; import org.apache.dolphinscheduler.dao.entity.event.ServerDownListenerEvent; import org.apache.dolphinscheduler.dao.mapper.AlertPluginInstanceMapper; -import org.apache.dolphinscheduler.dao.mapper.ListenerEventMapper; +import org.apache.dolphinscheduler.dao.repository.ListenerEventDao; import org.apache.commons.codec.digest.DigestUtils; @@ -43,39 +43,32 @@ import java.util.Optional; 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.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.mockito.junit.jupiter.MockitoExtension; -public class ListenerEventPostServiceTest { - - private static final Logger logger = LoggerFactory.getLogger(ListenerEventPostServiceTest.class); +@ExtendWith(MockitoExtension.class) +class ListenerEventSenderTest { @Mock - private ListenerEventMapper listenerEventMapper; + private ListenerEventDao listenerEventDao; + @Mock private AlertPluginInstanceMapper alertPluginInstanceMapper; @Mock private AlertPluginManager alertPluginManager; + @Mock private AlertConfig alertConfig; @InjectMocks - private ListenerEventPostService listenerEventPostService; - - @BeforeEach - public void before() { - MockitoAnnotations.initMocks(this); - } + private ListenerEventSender listenerEventSender; @Test - public void testSendServerDownEventSuccess() { - List events = new ArrayList<>(); + void testSendServerDownEventSuccess() { ServerDownListenerEvent serverDownListenerEvent = new ServerDownListenerEvent(); serverDownListenerEvent.setEventTime(new Date()); serverDownListenerEvent.setType("WORKER"); @@ -88,7 +81,6 @@ public void testSendServerDownEventSuccess() { successEvent.setEventType(ListenerEventType.SERVER_DOWN); successEvent.setCreateTime(new Date()); successEvent.setUpdateTime(new Date()); - events.add(successEvent); int pluginDefineId = 1; String pluginInstanceParams = @@ -103,19 +95,17 @@ public void testSendServerDownEventSuccess() { when(alertPluginInstanceMapper.queryAllGlobalAlertPluginInstanceList()).thenReturn(alertInstanceList); AlertResult sendResult = new AlertResult(); - sendResult.setStatus(String.valueOf(true)); + sendResult.setSuccess(true); sendResult.setMessage(String.format("Alert Plugin %s send success", pluginInstanceName)); AlertChannel alertChannelMock = mock(AlertChannel.class); when(alertChannelMock.process(Mockito.any())).thenReturn(sendResult); when(alertPluginManager.getAlertChannel(1)).thenReturn(Optional.of(alertChannelMock)); - Assertions.assertTrue(Boolean.parseBoolean(sendResult.getStatus())); - when(listenerEventMapper.deleteById(1)).thenReturn(1); - listenerEventPostService.send(events); + Assertions.assertTrue(sendResult.isSuccess()); + listenerEventSender.sendEvent(successEvent); } @Test - public void testSendServerDownEventFailed() { - List events = new ArrayList<>(); + void testSendServerDownEventFailed() { ServerDownListenerEvent serverDownListenerEvent = new ServerDownListenerEvent(); serverDownListenerEvent.setEventTime(new Date()); serverDownListenerEvent.setType("WORKER"); @@ -128,7 +118,6 @@ public void testSendServerDownEventFailed() { successEvent.setEventType(ListenerEventType.SERVER_DOWN); successEvent.setCreateTime(new Date()); successEvent.setUpdateTime(new Date()); - events.add(successEvent); int pluginDefineId = 1; String pluginInstanceParams = @@ -143,12 +132,12 @@ public void testSendServerDownEventFailed() { when(alertPluginInstanceMapper.queryAllGlobalAlertPluginInstanceList()).thenReturn(alertInstanceList); AlertResult sendResult = new AlertResult(); - sendResult.setStatus(String.valueOf(false)); + sendResult.setSuccess(false); sendResult.setMessage(String.format("Alert Plugin %s send failed", pluginInstanceName)); AlertChannel alertChannelMock = mock(AlertChannel.class); when(alertChannelMock.process(Mockito.any())).thenReturn(sendResult); when(alertPluginManager.getAlertChannel(1)).thenReturn(Optional.of(alertChannelMock)); - Assertions.assertFalse(Boolean.parseBoolean(sendResult.getStatus())); - listenerEventPostService.send(events); + Assertions.assertFalse(sendResult.isSuccess()); + listenerEventSender.sendEvent(successEvent); } } diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/service/AlertEventPendingQueueTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/service/AlertEventPendingQueueTest.java new file mode 100644 index 000000000000..a643e50e760c --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/service/AlertEventPendingQueueTest.java @@ -0,0 +1,94 @@ +/* + * 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.alert.service; + +import static com.google.common.truth.Truth.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import org.apache.dolphinscheduler.alert.config.AlertConfig; +import org.apache.dolphinscheduler.dao.entity.Alert; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import lombok.SneakyThrows; + +import org.awaitility.core.ConditionTimeoutException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AlertEventPendingQueueTest { + + private AlertEventPendingQueue alertEventPendingQueue; + + private static final int QUEUE_SIZE = 10; + + @BeforeEach + public void before() { + AlertConfig alertConfig = new AlertConfig(); + alertConfig.setSenderParallelism(QUEUE_SIZE); + this.alertEventPendingQueue = new AlertEventPendingQueue(alertConfig); + } + + @SneakyThrows + @Test + void put() { + for (int i = 0; i < alertEventPendingQueue.capacity(); i++) { + alertEventPendingQueue.put(new Alert()); + } + + CompletableFuture completableFuture = CompletableFuture.runAsync(() -> { + try { + alertEventPendingQueue.put(new Alert()); + System.out.println(alertEventPendingQueue.size()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + assertThrowsExactly(ConditionTimeoutException.class, + () -> await() + .timeout(Duration.ofSeconds(2)) + .until(completableFuture::isDone)); + + } + + @Test + void take() { + CompletableFuture completableFuture = CompletableFuture.runAsync(() -> { + try { + alertEventPendingQueue.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + assertThrowsExactly(ConditionTimeoutException.class, + () -> await() + .timeout(Duration.ofSeconds(2)) + .until(completableFuture::isDone)); + } + + @SneakyThrows + @Test + void size() { + for (int i = 0; i < alertEventPendingQueue.capacity(); i++) { + alertEventPendingQueue.put(new Alert()); + assertThat(alertEventPendingQueue.size()).isEqualTo(i + 1); + } + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/service/AlertSenderThreadPoolFactoryTest.java b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/service/AlertSenderThreadPoolFactoryTest.java new file mode 100644 index 000000000000..50f44fb43f31 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/java/org/apache/dolphinscheduler/alert/service/AlertSenderThreadPoolFactoryTest.java @@ -0,0 +1,44 @@ +/* + * 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.alert.service; + +import static com.google.common.truth.Truth.assertThat; + +import org.apache.dolphinscheduler.alert.config.AlertConfig; + +import java.util.concurrent.ThreadPoolExecutor; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AlertSenderThreadPoolFactoryTest { + + private final AlertConfig alertConfig = new AlertConfig(); + + private final AlertSenderThreadPoolFactory alertSenderThreadPoolFactory = + new AlertSenderThreadPoolFactory(alertConfig); + + @Test + void getThreadPool() { + ThreadPoolExecutor threadPool = alertSenderThreadPoolFactory.getThreadPool(); + assertThat(threadPool.getCorePoolSize()).isEqualTo(alertConfig.getSenderParallelism()); + assertThat(threadPool.getMaximumPoolSize()).isEqualTo(alertConfig.getSenderParallelism()); + } +} diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/resources/application.yaml b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/resources/application.yaml new file mode 100644 index 000000000000..d16d05a678ac --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/resources/application.yaml @@ -0,0 +1,107 @@ +# +# 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. +# + +spring: + profiles: + active: postgresql + jackson: + time-zone: UTC + date-format: "yyyy-MM-dd HH:mm:ss" + banner: + charset: UTF-8 + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://127.0.0.1:5432/dolphinscheduler + username: root + password: root + hikari: + connection-test-query: select 1 + pool-name: DolphinScheduler + +# Mybatis-plus configuration, you don't need to change it +mybatis-plus: + mapper-locations: classpath:org/apache/dolphinscheduler/dao/mapper/*Mapper.xml + type-aliases-package: org.apache.dolphinscheduler.dao.entity + configuration: + cache-enabled: false + call-setters-on-nulls: true + map-underscore-to-camel-case: true + jdbc-type-for-null: NULL + global-config: + db-config: + id-type: auto + banner: false + +server: + port: 50053 + +management: + endpoints: + web: + exposure: + include: health,metrics,prometheus + endpoint: + health: + enabled: true + show-details: always + health: + db: + enabled: true + defaults: + enabled: false + metrics: + tags: + application: ${spring.application.name} + +alert: + port: 50052 + # Mark each alert of alert server if late after x milliseconds as failed. + # Define value is (0 = infinite), and alert server would be waiting alert result. + wait-timeout: 10 + max-heartbeat-interval: 59s + # The maximum number of alerts that can be processed in parallel + sender-parallelism: 101 + +registry: + type: zookeeper + zookeeper: + namespace: dolphinscheduler + connect-string: localhost:2181 + retry-policy: + base-sleep-time: 60ms + max-sleep: 300ms + max-retries: 5 + session-timeout: 30s + connection-timeout: 9s + block-until-connected: 600ms + digest: ~ + +metrics: + enabled: true + +# Override by profile + +--- +spring: + config: + activate: + on-profile: mysql + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/dolphinscheduler + username: root + password: root diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/ServerStatus.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/ServerStatus.java index 1e4f49721a5e..afa7e97023e1 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/ServerStatus.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/ServerStatus.java @@ -20,6 +20,6 @@ public enum ServerStatus { NORMAL, - BUSY + BUSY, } diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/AlertServerHeartBeat.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/AlertServerHeartBeat.java index 9faaef82be4f..8533ca6e4843 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/AlertServerHeartBeat.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/model/AlertServerHeartBeat.java @@ -24,4 +24,9 @@ @NoArgsConstructor public class AlertServerHeartBeat extends BaseHeartBeat implements HeartBeat { + /** + * If the alert server is active or standby + */ + private boolean isActive; + } diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/AlertDao.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/AlertDao.java index 3b71312d0f13..d9f61098349f 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/AlertDao.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/AlertDao.java @@ -63,8 +63,7 @@ @Slf4j public class AlertDao { - @Value("${alert.query_alert_threshold:100}") - private Integer QUERY_ALERT_THRESHOLD; + private static final Integer QUERY_ALERT_THRESHOLD = 100; @Value("${alert.alarm-suppression.crash:60}") private Integer crashAlarmSuppression; @@ -104,8 +103,8 @@ public int addAlert(Alert alert) { * update alert sending(execution) status * * @param alertStatus alertStatus - * @param log alert results json - * @param id id + * @param log alert results json + * @param id id * @return update alert result */ public int updateAlert(AlertStatus alertStatus, String log, int id) { @@ -134,9 +133,9 @@ private String generateSign(Alert alert) { /** * add AlertSendStatus * - * @param sendStatus alert send status - * @param log log - * @param alertId alert id + * @param sendStatus alert send status + * @param log log + * @param alertId alert id * @param alertPluginInstanceId alert plugin instance id * @return insert count */ @@ -192,7 +191,7 @@ public void sendServerStoppedAlert(int alertGroupId, String host, String serverT * process time out alert * * @param processInstance processInstance - * @param projectUser projectUser + * @param projectUser projectUser */ public void sendProcessTimeoutAlert(ProcessInstance processInstance, ProjectUser projectUser) { int alertGroupId = processInstance.getWarningGroupId(); @@ -238,8 +237,8 @@ private void saveTaskTimeoutAlert(Alert alert, String content, int alertGroupId) * task timeout warn * * @param processInstance processInstanceId - * @param taskInstance taskInstance - * @param projectUser projectUser + * @param taskInstance taskInstance + * @param projectUser projectUser */ public void sendTaskTimeoutAlert(ProcessInstance processInstance, TaskInstance taskInstance, ProjectUser projectUser) { @@ -271,10 +270,11 @@ public void sendTaskTimeoutAlert(ProcessInstance processInstance, TaskInstance t } /** - * List alerts that are pending for execution + * List pending alerts which id > minAlertId and status = {@link AlertStatus#WAIT_EXECUTION} order by id asc. */ - public List listPendingAlerts() { - return alertMapper.listingAlertByStatus(AlertStatus.WAIT_EXECUTION.getCode(), QUERY_ALERT_THRESHOLD); + public List listPendingAlerts(int minAlertId) { + return alertMapper.listingAlertByStatus(minAlertId, AlertStatus.WAIT_EXECUTION.getCode(), + QUERY_ALERT_THRESHOLD); } public List listAlerts(int processInstanceId) { @@ -283,15 +283,6 @@ public List listAlerts(int processInstanceId) { return alertMapper.selectList(wrapper); } - /** - * for test - * - * @return AlertMapper - */ - public AlertMapper getAlertMapper() { - return alertMapper; - } - /** * list all alert plugin instance by alert group id * diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/AlertMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/AlertMapper.java index c30c1c9043e6..aab7b6f5f222 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/AlertMapper.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/AlertMapper.java @@ -34,9 +34,10 @@ public interface AlertMapper extends BaseMapper { /** - * Query the alert by alertStatus and return limit with default sort. + * Query the alert which id > minAlertId and status = alertStatus order by id asc. */ - List listingAlertByStatus(@Param("alertStatus") int alertStatus, @Param("limit") int limit); + List listingAlertByStatus(@Param("minAlertId") int minAlertId, @Param("alertStatus") int alertStatus, + @Param("limit") int limit); /** * Insert server crash alert diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapper.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapper.java index 820ac3b3a616..f3e187f8030e 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapper.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapper.java @@ -34,7 +34,8 @@ public interface ListenerEventMapper extends BaseMapper { void insertServerDownEvent(@Param("event") ListenerEvent event, @Param("crashAlarmSuppressionStartTime") Date crashAlarmSuppressionStartTime); - List listingListenerEventByStatus(@Param("postStatus") AlertStatus postStatus, + List listingListenerEventByStatus(@Param("minId") int minId, + @Param("postStatus") int postStatus, @Param("limit") int limit); void updateListenerEvent(@Param("eventId") int eventId, @Param("postStatus") AlertStatus postStatus, diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/ListenerEventDao.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/ListenerEventDao.java new file mode 100644 index 000000000000..424c616cd33e --- /dev/null +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/ListenerEventDao.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.dao.repository; + +import org.apache.dolphinscheduler.common.enums.AlertStatus; +import org.apache.dolphinscheduler.dao.entity.ListenerEvent; + +import java.util.Date; +import java.util.List; + +public interface ListenerEventDao extends IDao { + + List listingPendingEvents(int minId, int limit); + + void updateListenerEvent(int eventId, AlertStatus alertStatus, String message, Date date); +} diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/ListenerEventDaoImpl.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/ListenerEventDaoImpl.java new file mode 100644 index 000000000000..06c4dccd5e7b --- /dev/null +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/ListenerEventDaoImpl.java @@ -0,0 +1,51 @@ +/* + * 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.dao.repository.impl; + +import org.apache.dolphinscheduler.common.enums.AlertStatus; +import org.apache.dolphinscheduler.dao.entity.ListenerEvent; +import org.apache.dolphinscheduler.dao.mapper.ListenerEventMapper; +import org.apache.dolphinscheduler.dao.repository.BaseDao; +import org.apache.dolphinscheduler.dao.repository.ListenerEventDao; + +import java.util.Date; +import java.util.List; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +public class ListenerEventDaoImpl extends BaseDao implements ListenerEventDao { + + public ListenerEventDaoImpl(@NonNull ListenerEventMapper listenerEventMapper) { + super(listenerEventMapper); + } + + @Override + public List listingPendingEvents(int minId, int limit) { + return mybatisMapper.listingListenerEventByStatus(minId, AlertStatus.WAIT_EXECUTION.getCode(), limit); + } + + @Override + public void updateListenerEvent(int eventId, AlertStatus alertStatus, String message, Date date) { + mybatisMapper.updateListenerEvent(eventId, alertStatus, message, date); + } +} diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AlertMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AlertMapper.xml index e56afa830e61..f0c32aae78bd 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AlertMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AlertMapper.xml @@ -55,7 +55,9 @@ select from t_ds_alert - where alert_status = #{alertStatus} + where id > #{minAlertId} + and alert_status = #{alertStatus} + order by id asc limit #{limit} diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapper.xml index ae76bfcadd2e..8e6d3bbc3a9f 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapper.xml @@ -60,7 +60,8 @@ select from t_ds_listener_event - where post_status = #{postStatus.code} + where id > #{minId} and post_status = #{postStatus} + order by id asc limit #{limit} diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapperTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapperTest.java index f3e877aaa3ee..4e239265c424 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapperTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ListenerEventMapperTest.java @@ -81,7 +81,7 @@ public void testListingListenerEventByStatus() { ListenerEvent event2 = generateServerDownListenerEvent("192.168.x.2"); listenerEventMapper.batchInsert(Lists.newArrayList(event1, event2)); List listenerEvents = - listenerEventMapper.listingListenerEventByStatus(AlertStatus.WAIT_EXECUTION, 50); + listenerEventMapper.listingListenerEventByStatus(-1, AlertStatus.WAIT_EXECUTION.getCode(), 50); Assertions.assertEquals(listenerEvents.size(), 2); } @@ -111,8 +111,10 @@ public void testDeleteListenerEvent() { ListenerEvent actualAlert = listenerEventMapper.selectById(event.getId()); Assertions.assertNull(actualAlert); } + /** * create server down event + * * @param host worker host * @return listener event */ diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java index fe8545f3e2e0..a2c2c2ab9856 100644 --- a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/AlertDaoTest.java @@ -18,37 +18,23 @@ package org.apache.dolphinscheduler.dao.repository.impl; import org.apache.dolphinscheduler.common.enums.AlertStatus; -import org.apache.dolphinscheduler.common.enums.ProfileType; import org.apache.dolphinscheduler.dao.AlertDao; -import org.apache.dolphinscheduler.dao.DaoConfiguration; +import org.apache.dolphinscheduler.dao.BaseDaoTest; import org.apache.dolphinscheduler.dao.entity.Alert; import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.Rollback; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; -@ActiveProfiles(ProfileType.H2) -@ExtendWith(MockitoExtension.class) -@SpringBootApplication(scanBasePackageClasses = DaoConfiguration.class) -@SpringBootTest(classes = DaoConfiguration.class) -@Transactional -@Rollback -public class AlertDaoTest { +class AlertDaoTest extends BaseDaoTest { @Autowired private AlertDao alertDao; @Test - public void testAlertDao() { + void testAlertDao() { Alert alert = new Alert(); alert.setTitle("Mysql Exception"); alert.setContent("[\"alarm time:2018-02-05\", \"service name:MYSQL_ALTER\", \"alarm name:MYSQL_ALTER_DUMP\", " @@ -57,25 +43,25 @@ public void testAlertDao() { alert.setAlertStatus(AlertStatus.WAIT_EXECUTION); alertDao.addAlert(alert); - List alerts = alertDao.listPendingAlerts(); + List alerts = alertDao.listPendingAlerts(-1); Assertions.assertNotNull(alerts); Assertions.assertNotEquals(0, alerts.size()); } @Test - public void testAddAlertSendStatus() { + void testAddAlertSendStatus() { int insertCount = alertDao.addAlertSendStatus(AlertStatus.EXECUTION_SUCCESS, "success", 1, 1); Assertions.assertEquals(1, insertCount); } @Test - public void testSendServerStoppedAlert() { + void testSendServerStoppedAlert() { int alertGroupId = 1; String host = "127.0.0.998165432"; String serverType = "Master"; alertDao.sendServerStoppedAlert(alertGroupId, host, serverType); alertDao.sendServerStoppedAlert(alertGroupId, host, serverType); - long count = alertDao.listPendingAlerts() + long count = alertDao.listPendingAlerts(-1) .stream() .filter(alert -> alert.getContent().contains(host)) .count(); diff --git a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/ListenerEventDaoImplTest.java b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/ListenerEventDaoImplTest.java new file mode 100644 index 000000000000..2574c52f7828 --- /dev/null +++ b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/repository/impl/ListenerEventDaoImplTest.java @@ -0,0 +1,79 @@ +/* + * 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.dao.repository.impl; + +import static com.google.common.truth.Truth.assertThat; + +import org.apache.dolphinscheduler.common.enums.AlertStatus; +import org.apache.dolphinscheduler.common.enums.ListenerEventType; +import org.apache.dolphinscheduler.dao.BaseDaoTest; +import org.apache.dolphinscheduler.dao.entity.ListenerEvent; +import org.apache.dolphinscheduler.dao.repository.ListenerEventDao; + +import java.util.Date; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class ListenerEventDaoImplTest extends BaseDaoTest { + + @Autowired + private ListenerEventDao listenerEventDao; + + @Test + void listingPendingEvents() { + int minId = -1; + int limit = 10; + assertThat(listenerEventDao.listingPendingEvents(minId, limit)).isEmpty(); + + ListenerEvent listenerEvent = ListenerEvent.builder() + .eventType(ListenerEventType.SERVER_DOWN) + .sign("test") + .createTime(new Date()) + .updateTime(new Date()) + .postStatus(AlertStatus.WAIT_EXECUTION) + .build(); + listenerEventDao.insert(listenerEvent); + + listenerEvent = ListenerEvent.builder() + .eventType(ListenerEventType.SERVER_DOWN) + .sign("test") + .createTime(new Date()) + .updateTime(new Date()) + .postStatus(AlertStatus.EXECUTION_SUCCESS) + .build(); + listenerEventDao.insert(listenerEvent); + + assertThat(listenerEventDao.listingPendingEvents(minId, limit)).hasSize(1); + } + + @Test + void updateListenerEvent() { + ListenerEvent listenerEvent = ListenerEvent.builder() + .eventType(ListenerEventType.SERVER_DOWN) + .sign("test") + .createTime(new Date()) + .updateTime(new Date()) + .postStatus(AlertStatus.WAIT_EXECUTION) + .build(); + listenerEventDao.insert(listenerEvent); + listenerEventDao.updateListenerEvent(listenerEvent.getId(), AlertStatus.EXECUTION_SUCCESS, "test", new Date()); + assertThat(listenerEventDao.queryById(listenerEvent.getId()).getPostStatus()) + .isEqualTo(AlertStatus.EXECUTION_SUCCESS); + } +} diff --git a/dolphinscheduler-extract/dolphinscheduler-extract-alert/src/main/java/org/apache/dolphinscheduler/extract/alert/request/AlertSendResponse.java b/dolphinscheduler-extract/dolphinscheduler-extract-alert/src/main/java/org/apache/dolphinscheduler/extract/alert/request/AlertSendResponse.java index 832f3e1fabda..e1db2a233e20 100644 --- a/dolphinscheduler-extract/dolphinscheduler-extract-alert/src/main/java/org/apache/dolphinscheduler/extract/alert/request/AlertSendResponse.java +++ b/dolphinscheduler-extract/dolphinscheduler-extract-alert/src/main/java/org/apache/dolphinscheduler/extract/alert/request/AlertSendResponse.java @@ -37,6 +37,14 @@ public class AlertSendResponse { private List resResults; + public static AlertSendResponse success(List resResults) { + return new AlertSendResponse(true, resResults); + } + + public static AlertSendResponse fail(List resResults) { + return new AlertSendResponse(false, resResults); + } + @Data @NoArgsConstructor @AllArgsConstructor @@ -46,6 +54,14 @@ public static class AlertSendResponseResult implements Serializable { private String message; + public static AlertSendResponseResult success() { + return new AlertSendResponseResult(true, null); + } + + public static AlertSendResponseResult fail(String message) { + return new AlertSendResponseResult(false, message); + } + } } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/Registry.java b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/Registry.java index 8bdb8b9021af..86b82a8fb6b3 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/Registry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/Registry.java @@ -58,7 +58,6 @@ public interface Registry extends Closeable { String get(String key); /** - * * @param key * @param value * @param deleteOnDisconnect if true, when the connection state is disconnected, the key will be deleted @@ -67,6 +66,7 @@ public interface Registry extends Closeable { /** * This function will delete the keys whose prefix is {@param key} + * * @param key the prefix of deleted key * @throws if the key not exists, there is a registryException */ @@ -90,6 +90,11 @@ public interface Registry extends Closeable { */ boolean acquireLock(String key); + /** + * Acquire the lock of the prefix {@param key}, if acquire in the given timeout return true, else return false. + */ + boolean acquireLock(String key, long timeout); + /** * Release the lock of the prefix {@param key} */ diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/AbstractHAServer.java b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/AbstractHAServer.java new file mode 100644 index 000000000000..5dca5552b325 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/AbstractHAServer.java @@ -0,0 +1,105 @@ +/* + * 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.registry.api.ha; + +import org.apache.dolphinscheduler.common.thread.ThreadUtils; +import org.apache.dolphinscheduler.registry.api.Event; +import org.apache.dolphinscheduler.registry.api.Registry; + +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import com.google.common.collect.Lists; + +@Slf4j +public abstract class AbstractHAServer implements HAServer { + + private final Registry registry; + + private final String serverPath; + + private ServerStatus serverStatus; + + private final List serverStatusChangeListeners; + + public AbstractHAServer(Registry registry, String serverPath) { + this.registry = registry; + this.serverPath = serverPath; + this.serverStatus = ServerStatus.STAND_BY; + this.serverStatusChangeListeners = Lists.newArrayList(new DefaultServerStatusChangeListener()); + } + + @Override + public void start() { + registry.subscribe(serverPath, event -> { + if (Event.Type.REMOVE.equals(event.type())) { + if (isActive() && !participateElection()) { + statusChange(ServerStatus.STAND_BY); + } + } + }); + ScheduledExecutorService electionSelectionThread = + ThreadUtils.newSingleDaemonScheduledExecutorService("election-selection-thread"); + electionSelectionThread.schedule(() -> { + if (isActive()) { + return; + } + if (participateElection()) { + statusChange(ServerStatus.ACTIVE); + } + }, 10, TimeUnit.SECONDS); + } + + @Override + public boolean isActive() { + return ServerStatus.ACTIVE.equals(getServerStatus()); + } + + @Override + public boolean participateElection() { + return registry.acquireLock(serverPath, 3_000); + } + + @Override + public void addServerStatusChangeListener(ServerStatusChangeListener listener) { + serverStatusChangeListeners.add(listener); + } + + @Override + public ServerStatus getServerStatus() { + return serverStatus; + } + + @Override + public void shutdown() { + if (isActive()) { + registry.releaseLock(serverPath); + } + } + + private void statusChange(ServerStatus targetStatus) { + synchronized (this) { + ServerStatus originStatus = serverStatus; + serverStatus = targetStatus; + serverStatusChangeListeners.forEach(listener -> listener.change(originStatus, serverStatus)); + } + } +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/AbstractServerStatusChangeListener.java b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/AbstractServerStatusChangeListener.java new file mode 100644 index 000000000000..f2e332ea2068 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/AbstractServerStatusChangeListener.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.dolphinscheduler.registry.api.ha; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class AbstractServerStatusChangeListener implements ServerStatusChangeListener { + + @Override + public void change(HAServer.ServerStatus originStatus, HAServer.ServerStatus currentStatus) { + log.info("The status change from {} to {}.", originStatus, currentStatus); + if (originStatus == HAServer.ServerStatus.ACTIVE) { + if (currentStatus == HAServer.ServerStatus.STAND_BY) { + changeToStandBy(); + } + } else if (originStatus == HAServer.ServerStatus.STAND_BY) { + if (currentStatus == HAServer.ServerStatus.ACTIVE) { + changeToActive(); + } + } + } + + public abstract void changeToActive(); + + public abstract void changeToStandBy(); +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/DefaultServerStatusChangeListener.java b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/DefaultServerStatusChangeListener.java new file mode 100644 index 000000000000..d2acbcb51693 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/DefaultServerStatusChangeListener.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.dolphinscheduler.registry.api.ha; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DefaultServerStatusChangeListener extends AbstractServerStatusChangeListener { + + @Override + public void changeToActive() { + log.info("The status is active now."); + } + + @Override + public void changeToStandBy() { + log.info("The status is standby now."); + } +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/HAServer.java b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/HAServer.java new file mode 100644 index 000000000000..6a79e6eb844b --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/HAServer.java @@ -0,0 +1,68 @@ +/* + * 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.registry.api.ha; + +/** + * Interface for HA server, used to select a active server from multiple servers. + * In HA mode, there are multiple servers, only one server is active, others are standby. + */ +public interface HAServer { + + /** + * Start the server. + */ + void start(); + + /** + * Judge whether the server is active. + * + * @return true if the current server is active. + */ + boolean isActive(); + + /** + * Participate in the election of active server, this method will block until the server is active. + */ + boolean participateElection(); + + /** + * Add a listener to listen to the status change of the server. + * + * @param listener listener to add. + */ + void addServerStatusChangeListener(ServerStatusChangeListener listener); + + /** + * Get the status of the server. + * + * @return the status of the server. + */ + ServerStatus getServerStatus(); + + /** + * Shutdown the server, release resources. + */ + void shutdown(); + + enum ServerStatus { + ACTIVE, + STAND_BY, + ; + } + +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/ServerStatusChangeListener.java b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/ServerStatusChangeListener.java new file mode 100644 index 000000000000..af109228e230 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/ha/ServerStatusChangeListener.java @@ -0,0 +1,24 @@ +/* + * 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.registry.api.ha; + +public interface ServerStatusChangeListener { + + void change(HAServer.ServerStatus originStatus, HAServer.ServerStatus currentStatus); + +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java index 6833a6607b17..24a462c03fbb 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java @@ -34,6 +34,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import javax.net.ssl.SSLException; @@ -311,6 +313,35 @@ public boolean acquireLock(String key) { } } + @Override + public boolean acquireLock(String key, long timeout) { + Lock lockClient = client.getLockClient(); + Lease leaseClient = client.getLeaseClient(); + // get the lock with a lease + try { + long leaseId = leaseClient.grant(TIME_TO_LIVE_SECONDS).get().getID(); + // keep the lease + lockClient.lock(byteSequence(key), leaseId).get(timeout, TimeUnit.MICROSECONDS); + client.getLeaseClient().keepAlive(leaseId, Observers.observer(response -> { + })); + + // save the leaseId for release Lock + if (null == threadLocalLockMap.get()) { + threadLocalLockMap.set(new HashMap<>()); + } + threadLocalLockMap.get().put(key, leaseId); + return true; + } catch (TimeoutException timeoutException) { + log.debug("Acquire lock: {} in {}/ms timeout", key, timeout); + return false; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RegistryException("etcd get lock error", e); + } catch (ExecutionException e) { + throw new RegistryException("etcd get lock error, lockKey: " + key, e); + } + } + /** * release the lock by revoking the leaseId */ diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdKeepAliveLeaseManagerTest.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdKeepAliveLeaseManagerTest.java index 70593e4bad24..eb716378d098 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdKeepAliveLeaseManagerTest.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdKeepAliveLeaseManagerTest.java @@ -36,6 +36,7 @@ class EtcdKeepAliveLeaseManagerTest { static Client client; static EtcdKeepAliveLeaseManager etcdKeepAliveLeaseManager; + @BeforeAll public static void before() throws Exception { server = EtcdClusterExtension.builder() @@ -65,8 +66,9 @@ void getOrCreateKeepAliveLeaseTest() throws Exception { @AfterAll public static void after() throws IOException { - try (EtcdCluster closeServer = server.cluster()) { - client.close(); + try ( + EtcdCluster closeServer = server.cluster(); + Client closedClient = client) { } } } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistry.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistry.java index f3cbcfbc3b1d..12b29c34cc1d 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistry.java @@ -179,6 +179,17 @@ public boolean acquireLock(String key) { } } + @Override + public boolean acquireLock(String key, long timeout) { + try { + return registryLockManager.acquireLock(key, timeout); + } catch (RegistryException e) { + throw e; + } catch (Exception e) { + throw new RegistryException(String.format("Acquire lock: %s error", key), e); + } + } + @Override public boolean releaseLock(String key) { registryLockManager.releaseLock(key); diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/RegistryLockManager.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/RegistryLockManager.java index 46ccd15ec010..b624b9e78828 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/RegistryLockManager.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/RegistryLockManager.java @@ -83,6 +83,30 @@ public void acquireLock(String lockKey) throws RegistryException { }); } + /** + * Acquire the lock, if cannot get the lock will await. + */ + public boolean acquireLock(String lockKey, long timeout) throws RegistryException { + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < timeout) { + try { + if (lockHoldMap.containsKey(lockKey)) { + return true; + } + JdbcRegistryLock jdbcRegistryLock = jdbcOperator.tryToAcquireLock(lockKey); + if (jdbcRegistryLock != null) { + lockHoldMap.put(lockKey, jdbcRegistryLock); + return true; + } + } catch (SQLException e) { + throw new RegistryException("Acquire the lock: " + lockKey + " error", e); + } + log.debug("Acquire the lock {} failed try again", lockKey); + ThreadUtils.sleep(JdbcRegistryConstant.LOCK_ACQUIRE_INTERVAL); + } + return false; + } + public void releaseLock(String lockKey) { JdbcRegistryLock jdbcRegistryLock = lockHoldMap.get(lockKey); if (jdbcRegistryLock != null) { diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistry.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistry.java index 3f0c3ccb59df..38c211dfe7ff 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/main/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistry.java @@ -217,11 +217,41 @@ public void delete(String nodePath) { public boolean acquireLock(String key) { InterProcessMutex interProcessMutex = new InterProcessMutex(client, key); try { + if (interProcessMutex.isAcquiredInThisProcess()) { + return true; + } + Map processMutexMap = threadLocalLockMap.get(); + if (null == processMutexMap) { + processMutexMap = new HashMap<>(); + threadLocalLockMap.set(processMutexMap); + } interProcessMutex.acquire(); - if (null == threadLocalLockMap.get()) { - threadLocalLockMap.set(new HashMap<>(3)); + processMutexMap.put(key, interProcessMutex); + return true; + } catch (Exception e) { + try { + interProcessMutex.release(); + throw new RegistryException(String.format("zookeeper get lock: %s error", key), e); + } catch (Exception exception) { + throw new RegistryException(String.format("zookeeper get lock: %s error", key), e); } - threadLocalLockMap.get().put(key, interProcessMutex); + } + } + + @Override + public boolean acquireLock(String key, long timeout) { + InterProcessMutex interProcessMutex = new InterProcessMutex(client, key); + try { + if (interProcessMutex.isAcquiredInThisProcess()) { + return true; + } + Map processMutexMap = threadLocalLockMap.get(); + if (null == processMutexMap) { + processMutexMap = new HashMap<>(); + threadLocalLockMap.set(processMutexMap); + } + interProcessMutex.acquire(timeout, MILLISECONDS); + processMutexMap.put(key, interProcessMutex); return true; } catch (Exception e) { try { @@ -235,13 +265,17 @@ public boolean acquireLock(String key) { @Override public boolean releaseLock(String key) { - if (null == threadLocalLockMap.get().get(key)) { + Map processMutexMap = threadLocalLockMap.get(); + if (processMutexMap == null) { + return true; + } + if (null == processMutexMap.get(key)) { return false; } try { - threadLocalLockMap.get().get(key).release(); - threadLocalLockMap.get().remove(key); - if (threadLocalLockMap.get().isEmpty()) { + processMutexMap.get(key).release(); + processMutexMap.remove(key); + if (processMutexMap.isEmpty()) { threadLocalLockMap.remove(); } } catch (Exception e) { diff --git a/dolphinscheduler-standalone-server/src/main/resources/application.yaml b/dolphinscheduler-standalone-server/src/main/resources/application.yaml index 6757718929de..1c6da324eebd 100644 --- a/dolphinscheduler-standalone-server/src/main/resources/application.yaml +++ b/dolphinscheduler-standalone-server/src/main/resources/application.yaml @@ -232,7 +232,8 @@ alert: # Define value is (0 = infinite), and alert server would be waiting alert result. wait-timeout: 0 max-heartbeat-interval: 60s - query_alert_threshold: 100 + # The maximum number of alerts that can be processed in parallel + sender-parallelism: 5 api: audit-enable: false From ba5de75829f63f0816c9bf4fee7f7e1af4db6fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E9=98=B3=E9=98=B3=28liuyangyang=29?= <1024717602@qq.com> Date: Thu, 9 May 2024 13:06:22 +0800 Subject: [PATCH 067/165] Add tenantCode propagation to DynamicCommandUtils.createCommand (#15956) --- .../server/master/runner/task/dynamic/DynamicCommandUtils.java | 1 + .../master/runner/task/dynamic/DynamicCommandUtilsTest.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicCommandUtils.java b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicCommandUtils.java index e360a8857e26..2401562f15ab 100644 --- a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicCommandUtils.java +++ b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicCommandUtils.java @@ -65,6 +65,7 @@ static public Command createCommand(ProcessInstance processInstance, command.setProcessInstancePriority(processInstance.getProcessInstancePriority()); command.setWorkerGroup(processInstance.getWorkerGroup()); command.setDryRun(processInstance.getDryRun()); + command.setTenantCode(processInstance.getTenantCode()); return command; } diff --git a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicCommandUtilsTest.java b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicCommandUtilsTest.java index d238869f41f4..d9b9c82e6641 100644 --- a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicCommandUtilsTest.java +++ b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/runner/task/dynamic/DynamicCommandUtilsTest.java @@ -54,6 +54,7 @@ void setUp() { processInstance.setWarningGroupId(1); processInstance.setProcessInstancePriority(null); // update this processInstance.setWorkerGroup("worker"); + processInstance.setTenantCode("unit-root"); processInstance.setDryRun(0); } @@ -73,6 +74,7 @@ void testCreateCommand() { Assertions.assertEquals(processInstance.getProcessInstancePriority(), command.getProcessInstancePriority()); Assertions.assertEquals(processInstance.getWorkerGroup(), command.getWorkerGroup()); Assertions.assertEquals(processInstance.getDryRun(), command.getDryRun()); + Assertions.assertEquals(processInstance.getTenantCode(), command.getTenantCode()); } @Test From 5c569b705cad84be0b017dc94dfdd979318e3279 Mon Sep 17 00:00:00 2001 From: Zzih96 <158246610+Zzih96@users.noreply.github.com> Date: Thu, 9 May 2024 14:15:06 +0800 Subject: [PATCH 068/165] [fix-15907] Fix get remote shell exit code is incorrect (#15911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ./mvnw spotless:apply * Update dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java Co-authored-by: Wenjun Ruan --------- Co-authored-by: 詹子恒 Co-authored-by: Wenjun Ruan Co-authored-by: Rick Cheng --- .../plugin/task/remoteshell/RemoteExecutor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java index 307023043a4e..814826a6bb4f 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java @@ -131,7 +131,7 @@ public Integer getTaskExitCode(String taskId) throws IOException { int exitCode = -1; log.info("Remote shell task run status: {}", logLine); if (logLine.contains(STATUS_TAG_MESSAGE)) { - String status = logLine.replace(STATUS_TAG_MESSAGE, "").trim(); + String status = StringUtils.substringAfter(logLine, STATUS_TAG_MESSAGE); if (status.equals("0")) { log.info("Remote shell task success"); exitCode = 0; From 60b019b729a5bb1c05e5627d85b1a903546100a8 Mon Sep 17 00:00:00 2001 From: cntiger <35484811+cntigers@users.noreply.github.com> Date: Thu, 9 May 2024 14:50:27 +0800 Subject: [PATCH 069/165] [Improvement] Fix the git url command injection in pytorch task(#15873) (#15950) * fix the git url command injection danger(#15873) * [Improvement] Fix the git url command injection in pytorch,format code style task(#15873) --------- Co-authored-by: cntigers Co-authored-by: Rick Cheng --- .../plugin/task/pytorch/GitProjectManager.java | 4 ++-- .../plugin/task/pytorch/PytorchTaskTest.java | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-pytorch/src/main/java/org/apache/dolphinscheduler/plugin/task/pytorch/GitProjectManager.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-pytorch/src/main/java/org/apache/dolphinscheduler/plugin/task/pytorch/GitProjectManager.java index 3189f269202c..5f1e815c30bc 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-pytorch/src/main/java/org/apache/dolphinscheduler/plugin/task/pytorch/GitProjectManager.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-pytorch/src/main/java/org/apache/dolphinscheduler/plugin/task/pytorch/GitProjectManager.java @@ -33,12 +33,12 @@ public class GitProjectManager { public static final String GIT_PATH_LOCAL = "GIT_PROJECT"; - private static final Pattern GIT_CHECK_PATTERN = Pattern.compile("^(git@|https?://)"); + private static final Pattern GIT_CHECK_PATTERN = Pattern.compile("^(git@|https?://)(?![&|])[^&|]+$"); private String path; private String baseDir = "."; public static boolean isGitPath(String path) { - return GIT_CHECK_PATTERN.matcher(path).find(); + return GIT_CHECK_PATTERN.matcher(path).matches(); } public void prepareProject() throws Exception { diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-pytorch/src/test/java/org/apache/dolphinscheduler/plugin/task/pytorch/PytorchTaskTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-pytorch/src/test/java/org/apache/dolphinscheduler/plugin/task/pytorch/PytorchTaskTest.java index c213021607b1..e35a175df165 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-pytorch/src/test/java/org/apache/dolphinscheduler/plugin/task/pytorch/PytorchTaskTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-pytorch/src/test/java/org/apache/dolphinscheduler/plugin/task/pytorch/PytorchTaskTest.java @@ -72,6 +72,12 @@ public void testPythonEnvManager() { } + @Test + public void testGitProjectUrlInjection() { + Assertions.assertFalse(GitProjectManager.isGitPath("git@& cat /etc/passwd >/poc.txt #")); + Assertions.assertFalse(GitProjectManager.isGitPath("git@| cat /etc/passwd >/poc.txt #")); + } + @Test public void testGitProject() { From ad1a6af4fb24ddc17bc092f20453de01d1128311 Mon Sep 17 00:00:00 2001 From: JohnHuang Date: Thu, 9 May 2024 16:52:37 +0800 Subject: [PATCH 070/165] Add link to ETCD/JDBC Registry Guideline (#15597) --- docs/docs/en/architecture/configuration.md | 5 ++++- docs/docs/zh/architecture/configuration.md | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/docs/en/architecture/configuration.md b/docs/docs/en/architecture/configuration.md index fe0b7851bae1..cc3bc94fc0ea 100644 --- a/docs/docs/en/architecture/configuration.md +++ b/docs/docs/en/architecture/configuration.md @@ -165,7 +165,7 @@ The default configuration is as follows: Note that DolphinScheduler also supports database configuration through `bin/env/dolphinscheduler_env.sh`. -### Zookeeper related configuration +### Registry Related configuration DolphinScheduler uses Zookeeper for cluster management, fault tolerance, event monitoring and other functions. Configuration file location: @@ -191,6 +191,9 @@ The default configuration is as follows: Note that DolphinScheduler also supports zookeeper related configuration through `bin/env/dolphinscheduler_env.sh`. +For ETCD Registry, please see more details on [link](https://github.com/apache/dolphinscheduler/blob/dev/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/README.md). +For JDBC Registry, please see more details on [link](https://github.com/apache/dolphinscheduler/blob/dev/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/README.md). + ### common.properties [hadoop、s3、yarn config properties] Currently, common.properties mainly configures Hadoop,s3a related configurations. Configuration file location: diff --git a/docs/docs/zh/architecture/configuration.md b/docs/docs/zh/architecture/configuration.md index d8d1d42d1eb8..3e40b8d88f76 100644 --- a/docs/docs/zh/architecture/configuration.md +++ b/docs/docs/zh/architecture/configuration.md @@ -165,9 +165,9 @@ export DOLPHINSCHEDULER_OPTS=" DolphinScheduler同样可以通过设置环境变量进行数据库连接相关的配置, 将以上小写字母转成大写并把`.`换成`_`作为环境变量名, 设置值即可。 -## Zookeeper相关配置 +## 注册中心相关配置 -DolphinScheduler使用Zookeeper进行集群管理、容错、事件监听等功能,配置文件位置: +DolphinScheduler默认使用Zookeeper进行集群管理、容错、事件监听等功能,配置文件位置: |服务名称| 配置文件 | |--|--| |Master Server | `master-server/conf/application.yaml`| @@ -190,6 +190,9 @@ DolphinScheduler使用Zookeeper进行集群管理、容错、事件监听等功 DolphinScheduler同样可以通过`bin/env/dolphinscheduler_env.sh`进行Zookeeper相关的配置。 +如果使用etcd作为注册中心,详细请参考[链接](https://github.com/apache/dolphinscheduler/blob/dev/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/README.md)。 +如果使用jdbc作为注册中心,详细请参考[链接](https://github.com/apache/dolphinscheduler/blob/dev/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/README.md)。 + ## common.properties [hadoop、s3、yarn配置] common.properties配置文件目前主要是配置hadoop/s3/yarn/applicationId收集相关的配置,配置文件位置: From ace20f96c27215d5d8c84ee0b193bfc1672c8ae3 Mon Sep 17 00:00:00 2001 From: Gallardot Date: Fri, 10 May 2024 15:43:00 +0800 Subject: [PATCH 071/165] [Bug] [Helm] No DB Nodes Exist (#15970) --- deploy/kubernetes/dolphinscheduler/templates/_helpers.tpl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/kubernetes/dolphinscheduler/templates/_helpers.tpl b/deploy/kubernetes/dolphinscheduler/templates/_helpers.tpl index 0b2a542cc1d0..71287b1f1081 100644 --- a/deploy/kubernetes/dolphinscheduler/templates/_helpers.tpl +++ b/deploy/kubernetes/dolphinscheduler/templates/_helpers.tpl @@ -146,6 +146,10 @@ Create a database environment variables. {{- else }} value: {{ .Values.externalDatabase.type | quote }} {{- end }} +{{- if or .Values.mysql.enabled (eq .Values.externalDatabase.type "mysql") }} +- name: SPRING_PROFILES_ACTIVE + value: mysql +{{- end }} - name: SPRING_DATASOURCE_URL {{- if .Values.postgresql.enabled }} value: jdbc:postgresql://{{ template "dolphinscheduler.postgresql.fullname" . }}:5432/{{ .Values.postgresql.postgresqlDatabase }}?{{ .Values.postgresql.params }} From 3446fd8ab157974e31226635e277b6c56b6b7cb5 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Fri, 10 May 2024 17:34:26 +0800 Subject: [PATCH 072/165] EMR task support replace params placeholder (#15975) Co-authored-by: Eric Gao --- .../plugin/task/emr/EmrAddStepsTask.java | 9 +++++++-- .../dolphinscheduler/plugin/task/emr/EmrJobFlowTask.java | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr/src/main/java/org/apache/dolphinscheduler/plugin/task/emr/EmrAddStepsTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr/src/main/java/org/apache/dolphinscheduler/plugin/task/emr/EmrAddStepsTask.java index 753b206e21f8..13dc35c30a68 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr/src/main/java/org/apache/dolphinscheduler/plugin/task/emr/EmrAddStepsTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr/src/main/java/org/apache/dolphinscheduler/plugin/task/emr/EmrAddStepsTask.java @@ -20,6 +20,7 @@ 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.utils.ParameterUtils; import java.util.Collections; import java.util.HashSet; @@ -126,11 +127,15 @@ public void trackApplicationStatus() throws TaskException { protected AddJobFlowStepsRequest createAddJobFlowStepsRequest() { final AddJobFlowStepsRequest addJobFlowStepsRequest; + String jobStepDefineJson = null; try { + jobStepDefineJson = ParameterUtils.convertParameterPlaceholders( + emrParameters.getStepsDefineJson(), + ParameterUtils.convert(taskExecutionContext.getPrepareParamsMap())); addJobFlowStepsRequest = - objectMapper.readValue(emrParameters.getStepsDefineJson(), AddJobFlowStepsRequest.class); + objectMapper.readValue(jobStepDefineJson, AddJobFlowStepsRequest.class); } catch (JsonProcessingException e) { - throw new EmrTaskException("can not parse AddJobFlowStepsRequest from json", e); + throw new EmrTaskException("can not parse AddJobFlowStepsRequest from json: " + jobStepDefineJson, e); } // When a single task definition is associated with multiple steps, the state tracking will have high diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr/src/main/java/org/apache/dolphinscheduler/plugin/task/emr/EmrJobFlowTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr/src/main/java/org/apache/dolphinscheduler/plugin/task/emr/EmrJobFlowTask.java index f4b05340652e..8b772a1118f2 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-emr/src/main/java/org/apache/dolphinscheduler/plugin/task/emr/EmrJobFlowTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-emr/src/main/java/org/apache/dolphinscheduler/plugin/task/emr/EmrJobFlowTask.java @@ -20,6 +20,7 @@ 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.utils.ParameterUtils; import java.util.Collections; import java.util.HashSet; @@ -120,10 +121,14 @@ public void trackApplicationStatus() throws TaskException { protected RunJobFlowRequest createRunJobFlowRequest() { final RunJobFlowRequest runJobFlowRequest; + String jobFlowDefineJson = null; try { - runJobFlowRequest = objectMapper.readValue(emrParameters.getJobFlowDefineJson(), RunJobFlowRequest.class); + jobFlowDefineJson = ParameterUtils.convertParameterPlaceholders( + emrParameters.getJobFlowDefineJson(), + ParameterUtils.convert(taskExecutionContext.getPrepareParamsMap())); + runJobFlowRequest = objectMapper.readValue(jobFlowDefineJson, RunJobFlowRequest.class); } catch (JsonProcessingException e) { - throw new EmrTaskException("can not parse RunJobFlowRequest from json", e); + throw new EmrTaskException("can not parse RunJobFlowRequest from json: " + jobFlowDefineJson, e); } return runJobFlowRequest; From 7c8fa9b48cc680081d34d18954cf34099c7472d0 Mon Sep 17 00:00:00 2001 From: Wenjun Ruan Date: Mon, 13 May 2024 16:35:37 +0800 Subject: [PATCH 073/165] Add IntegretionTest for registry module (#15981) --- .github/workflows/unit-test.yml | 16 +- .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/application.yaml | 11 + .../{logback-spring.xml => logback.xml} | 0 dolphinscheduler-bom/pom.xml | 8 + .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../registry/api/Registry.java | 45 ++- .../dolphinscheduler-registry-etcd/pom.xml | 25 +- .../plugin/registry/etcd/EtcdRegistry.java | 55 ++-- .../registry/etcd/EtcdRegistryProperties.java | 2 + .../etcd/EtcdKeepAliveLeaseManagerTest.java | 2 +- .../registry/etcd/EtcdRegistryTest.java | 143 --------- .../registry/etcd/EtcdRegistryTestCase.java | 70 +++++ .../src/test/resources/application.yaml | 20 ++ .../src/test/resources/logback.xml | 21 ++ .../dolphinscheduler-registry-it/pom.xml | 60 ++++ .../plugin/registry/RegistryTestCase.java | 290 ++++++++++++++++++ .../dolphinscheduler-registry-jdbc/pom.xml | 25 ++ .../jdbc/{task => }/EphemeralDateManager.java | 10 +- .../plugin/registry/jdbc/JdbcOperator.java | 54 ++-- .../plugin/registry/jdbc/JdbcRegistry.java | 38 +-- .../jdbc/JdbcRegistryAutoConfiguration.java | 12 + .../registry/jdbc/JdbcRegistryConstant.java | 6 +- .../plugin/registry/jdbc/LockUtils.java | 34 ++ .../jdbc/{task => }/RegistryLockManager.java | 57 ++-- .../jdbc/{task => }/SubscribeDataManager.java | 18 +- .../jdbc/mapper/JdbcRegistryDataMapper.java | 3 - .../jdbc/mapper/JdbcRegistryLockMapper.java | 2 +- .../main/resources/mysql_registry_init.sql | 4 +- .../registry/jdbc/JdbcRegistryTestCase.java | 41 +++ .../jdbc/MysqlJdbcRegistryTestCase.java | 103 +++++++ .../jdbc/PostgresqlJdbcRegistryTestCase.java | 98 ++++++ .../src/test/resources/application-mysql.yaml | 31 ++ .../resources/application-postgresql.yaml | 28 ++ .../src/test/resources/logback.xml | 21 ++ .../pom.xml | 37 ++- .../ZookeeperConnectionStateListener.java | 8 +- .../registry/zookeeper/ZookeeperRegistry.java | 96 +++--- .../ZookeeperRegistryAutoConfiguration.java | 4 +- .../ZookeeperRegistryProperties.java | 67 +++- .../zookeeper/ZookeeperRegistryTest.java | 131 -------- .../zookeeper/ZookeeperRegistryTestCase.java | 71 +++++ .../src/test/resources/application.yaml | 30 ++ .../src/test/resources/logback.xml | 21 ++ .../dolphinscheduler-registry-plugins/pom.xml | 1 + .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ .../src/test/resources/logback.xml | 21 ++ pom.xml | 24 +- 65 files changed, 1781 insertions(+), 503 deletions(-) create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/test/resources/logback.xml create mode 100644 dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/resources/logback.xml rename dolphinscheduler-api/src/test/resources/{logback-spring.xml => logback.xml} (100%) create mode 100644 dolphinscheduler-common/src/test/resources/logback.xml create mode 100644 dolphinscheduler-data-quality/src/test/resources/logback.xml delete mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryTest.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryTestCase.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/resources/application.yaml create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/resources/logback.xml create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-it/pom.xml create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-it/src/test/java/org/apache/dolphinscheduler/plugin/registry/RegistryTestCase.java rename dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/{task => }/EphemeralDateManager.java (93%) create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/LockUtils.java rename dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/{task => }/RegistryLockManager.java (73%) rename dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/{task => }/SubscribeDataManager.java (91%) create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/test/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryTestCase.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/test/java/org/apache/dolphinscheduler/plugin/registry/jdbc/MysqlJdbcRegistryTestCase.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/test/java/org/apache/dolphinscheduler/plugin/registry/jdbc/PostgresqlJdbcRegistryTestCase.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/test/resources/application-mysql.yaml create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/test/resources/application-postgresql.yaml create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/test/resources/logback.xml delete mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/test/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryTest.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/test/java/org/apache/dolphinscheduler/plugin/registry/zookeeper/ZookeeperRegistryTestCase.java create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/test/resources/application.yaml create mode 100644 dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-zookeeper/src/test/resources/logback.xml create mode 100644 dolphinscheduler-storage-plugin/dolphinscheduler-storage-abs/src/test/resources/logback.xml create mode 100644 dolphinscheduler-storage-plugin/dolphinscheduler-storage-gcs/src/test/resources/logback.xml create mode 100644 dolphinscheduler-storage-plugin/dolphinscheduler-storage-hdfs/src/test/resources/logback.xml create mode 100644 dolphinscheduler-storage-plugin/dolphinscheduler-storage-obs/src/test/resources/logback.xml create mode 100644 dolphinscheduler-storage-plugin/dolphinscheduler-storage-oss/src/test/resources/logback.xml create mode 100644 dolphinscheduler-storage-plugin/dolphinscheduler-storage-s3/src/test/resources/logback.xml diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 6c9f41d7a34d..a7e78a11f76a 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -76,7 +76,7 @@ jobs: restore-keys: ${{ runner.os }}-maven- - name: Run Unit tests - run: ./mvnw clean verify -B -Dmaven.test.skip=false -Dspotless.skip=true -DskipUT=false -DskipIT=false + run: ./mvnw clean verify -B -Dmaven.test.skip=false -Dspotless.skip=true -DskipUT=false - name: Upload coverage report to codecov run: CODECOV_TOKEN="09c2663f-b091-4258-8a47-c981827eb29a" bash <(curl -s https://codecov.io/bash) @@ -99,23 +99,11 @@ jobs: -Dsonar.login=e4058004bc6be89decf558ac819aa1ecbee57682 -Dsonar.exclusions=,dolphinscheduler-ui/src/**/i18n/locale/*.js,dolphinscheduler-microbench/src/**/* -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 - -DskipUT=true -DskipIT=true + -DskipUT=true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: Collect logs - continue-on-error: true - run: | - mkdir -p ${LOG_DIR} - docker-compose -f $(pwd)/docker/docker-swarm/docker-compose.yml logs dolphinscheduler-postgresql > ${LOG_DIR}/db.txt - - - name: Upload logs - uses: actions/upload-artifact@v2 - continue-on-error: true - with: - name: unit-test-logs - path: ${LOG_DIR} result: name: Unit Test runs-on: ubuntu-latest diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-aliyunVoice/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-dingtalk/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-email/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-feishu/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-http/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-pagerduty/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-prometheus/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-script/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-slack/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-telegram/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-webexteams/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-plugins/dolphinscheduler-alert-wechat/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/resources/logback.xml b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-alert/dolphinscheduler-alert-server/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-api/src/test/resources/application.yaml b/dolphinscheduler-api/src/test/resources/application.yaml index 26536d631f68..5eb7e1f8d7e9 100644 --- a/dolphinscheduler-api/src/test/resources/application.yaml +++ b/dolphinscheduler-api/src/test/resources/application.yaml @@ -44,6 +44,17 @@ mybatis-plus: registry: type: zookeeper + zookeeper: + namespace: dolphinscheduler + connect-string: localhost:2181 + retry-policy: + base-sleep-time: 60ms + max-sleep: 300ms + max-retries: 5 + session-timeout: 30s + connection-timeout: 9s + block-until-connected: 600ms + digest: ~ api: audit-enable: true diff --git a/dolphinscheduler-api/src/test/resources/logback-spring.xml b/dolphinscheduler-api/src/test/resources/logback.xml similarity index 100% rename from dolphinscheduler-api/src/test/resources/logback-spring.xml rename to dolphinscheduler-api/src/test/resources/logback.xml diff --git a/dolphinscheduler-bom/pom.xml b/dolphinscheduler-bom/pom.xml index 18e75de57ba0..10c4f0e4a10a 100644 --- a/dolphinscheduler-bom/pom.xml +++ b/dolphinscheduler-bom/pom.xml @@ -37,6 +37,7 @@ 1.2.20 2.12.0 0.5.11 + 0.7.1 1.41.0 1.11 @@ -943,6 +944,13 @@ + + org.testcontainers + testcontainers + ${testcontainer.version} + test + + org.testcontainers mysql diff --git a/dolphinscheduler-common/src/test/resources/logback.xml b/dolphinscheduler-common/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-common/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-data-quality/src/test/resources/logback.xml b/dolphinscheduler-data-quality/src/test/resources/logback.xml new file mode 100644 index 000000000000..9a182a18ef12 --- /dev/null +++ b/dolphinscheduler-data-quality/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/Registry.java b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/Registry.java index 86b82a8fb6b3..f90ef1ea3243 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/Registry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/Registry.java @@ -26,10 +26,20 @@ import lombok.NonNull; /** - * Registry + * The SPI interface for registry center, each registry plugin should implement this interface. */ public interface Registry extends Closeable { + /** + * Start the registry, once started, the registry will connect to the registry center. + */ + void start(); + + /** + * Whether the registry is connected + * + * @return true if connected, false otherwise. + */ boolean isConnected(); /** @@ -40,7 +50,13 @@ public interface Registry extends Closeable { */ void connectUntilTimeout(@NonNull Duration timeout) throws RegistryException; - boolean subscribe(String path, SubscribeListener listener); + /** + * Subscribe the path, when the path has expose {@link Event}, the listener will be triggered. + * + * @param path the path to subscribe + * @param listener the listener to be triggered + */ + void subscribe(String path, SubscribeListener listener); /** * Remove the path from the subscribe list. @@ -53,35 +69,34 @@ public interface Registry extends Closeable { void addConnectionStateListener(ConnectionListener listener); /** - * @return the value + * Get the value of the key, if key not exist will throw {@link RegistryException} */ - String get(String key); + String get(String key) throws RegistryException; /** - * @param key - * @param value + * Put the key-value pair into the registry + * + * @param key the key, cannot be null + * @param value the value, cannot be null * @param deleteOnDisconnect if true, when the connection state is disconnected, the key will be deleted */ void put(String key, String value, boolean deleteOnDisconnect); /** - * This function will delete the keys whose prefix is {@param key} - * - * @param key the prefix of deleted key - * @throws if the key not exists, there is a registryException + * Delete the key from the registry */ void delete(String key); /** - * @return {@code true} if key exists. - * E.g: registry contains the following keys:[/test/test1/test2,] - * if the key: /test - * Return: test1 + * Return the children of the key */ Collection children(String key); /** - * @return if key exists,return true + * Check if the key exists + * + * @param key the key to check + * @return true if the key exists */ boolean exists(String key); diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/pom.xml b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/pom.xml index b084db1ccfee..0f5c4d1494ff 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/pom.xml +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/pom.xml @@ -31,6 +31,15 @@ org.apache.dolphinscheduler dolphinscheduler-registry-api + + + org.apache.dolphinscheduler + dolphinscheduler-registry-it + ${project.version} + test-jar + test + + io.etcd jetcd-core @@ -49,18 +58,22 @@ + + + io.netty + netty-all + + io.etcd jetcd-test test + - io.netty - netty-all - - - org.slf4j - slf4j-api + org.springframework.boot + spring-boot-starter-test + test diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java index 24a462c03fbb..80279775ffb9 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistry.java @@ -71,6 +71,7 @@ @Slf4j public class EtcdRegistry implements Registry { + private final EtcdRegistryProperties etcdRegistryProperties; private final Client client; private EtcdConnectionStateListener etcdConnectionStateListener; @@ -83,9 +84,8 @@ public class EtcdRegistry implements Registry { private final Map watcherMap = new ConcurrentHashMap<>(); - private static final long TIME_TO_LIVE_SECONDS = 30L; - public EtcdRegistry(EtcdRegistryProperties registryProperties) throws SSLException { + this.etcdRegistryProperties = registryProperties; ClientBuilder clientBuilder = Client.builder() .endpoints(Util.toURIs(Splitter.on(",").trimResults().splitToList(registryProperties.getEndpoints()))) .namespace(byteSequence(registryProperties.getNamespace())) @@ -129,6 +129,11 @@ public EtcdRegistry(EtcdRegistryProperties registryProperties) throws SSLExcepti } + @Override + public void start() { + // The start has been set in the constructor + } + @Override public boolean isConnected() { return client.getKVClient().get(byteSequence("/")).join() != null; @@ -145,7 +150,7 @@ public void connectUntilTimeout(@NonNull Duration timeout) throws RegistryExcept * @return if subcribe Returns true if no exception was thrown */ @Override - public boolean subscribe(String path, SubscribeListener listener) { + public void subscribe(String path, SubscribeListener listener) { try { ByteSequence watchKey = byteSequence(path); WatchOption watchOption = @@ -159,7 +164,6 @@ public boolean subscribe(String path, SubscribeListener listener) { } catch (Exception e) { throw new RegistryException("Failed to subscribe listener for key: " + path, e); } - return true; } /** @@ -193,7 +197,7 @@ public String get(String key) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RegistryException("etcd get data error", e); - } catch (ExecutionException e) { + } catch (Exception e) { throw new RegistryException("etcd get data error, key = " + key, e); } } @@ -206,7 +210,8 @@ public void put(String key, String value, boolean deleteOnDisconnect) { try { if (deleteOnDisconnect) { // keep the key by lease, if disconnected, the lease will expire and the key will delete - long leaseId = etcdKeepAliveLeaseManager.getOrCreateKeepAliveLease(key, TIME_TO_LIVE_SECONDS); + long leaseId = etcdKeepAliveLeaseManager.getOrCreateKeepAliveLease(key, + etcdRegistryProperties.getTtl().get(ChronoUnit.SECONDS)); PutOption putOption = PutOption.newBuilder().withLeaseId(leaseId).build(); client.getKVClient().put(byteSequence(key), byteSequence(value), putOption).get(); } else { @@ -289,47 +294,59 @@ public boolean exists(String key) { */ @Override public boolean acquireLock(String key) { + Map leaseIdMap = threadLocalLockMap.get(); + if (null == leaseIdMap) { + leaseIdMap = new HashMap<>(); + threadLocalLockMap.set(leaseIdMap); + } + if (leaseIdMap.containsKey(key)) { + return true; + } + Lock lockClient = client.getLockClient(); Lease leaseClient = client.getLeaseClient(); // get the lock with a lease try { - long leaseId = leaseClient.grant(TIME_TO_LIVE_SECONDS).get().getID(); + long leaseId = leaseClient.grant(etcdRegistryProperties.getTtl().get(ChronoUnit.SECONDS)).get().getID(); // keep the lease client.getLeaseClient().keepAlive(leaseId, Observers.observer(response -> { })); lockClient.lock(byteSequence(key), leaseId).get(); // save the leaseId for release Lock - if (null == threadLocalLockMap.get()) { - threadLocalLockMap.set(new HashMap<>()); - } - threadLocalLockMap.get().put(key, leaseId); + leaseIdMap.put(key, leaseId); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RegistryException("etcd get lock error", e); - } catch (ExecutionException e) { + } catch (Exception e) { throw new RegistryException("etcd get lock error, lockKey: " + key, e); } } @Override public boolean acquireLock(String key, long timeout) { + Map leaseIdMap = threadLocalLockMap.get(); + if (null == leaseIdMap) { + leaseIdMap = new HashMap<>(); + threadLocalLockMap.set(leaseIdMap); + } + if (leaseIdMap.containsKey(key)) { + return true; + } + Lock lockClient = client.getLockClient(); Lease leaseClient = client.getLeaseClient(); // get the lock with a lease try { - long leaseId = leaseClient.grant(TIME_TO_LIVE_SECONDS).get().getID(); + long leaseId = leaseClient.grant(etcdRegistryProperties.getTtl().get(ChronoUnit.SECONDS)).get().getID(); // keep the lease - lockClient.lock(byteSequence(key), leaseId).get(timeout, TimeUnit.MICROSECONDS); + lockClient.lock(byteSequence(key), leaseId).get(timeout, TimeUnit.MILLISECONDS); client.getLeaseClient().keepAlive(leaseId, Observers.observer(response -> { })); // save the leaseId for release Lock - if (null == threadLocalLockMap.get()) { - threadLocalLockMap.set(new HashMap<>()); - } - threadLocalLockMap.get().put(key, leaseId); + leaseIdMap.put(key, leaseId); return true; } catch (TimeoutException timeoutException) { log.debug("Acquire lock: {} in {}/ms timeout", key, timeout); @@ -337,7 +354,7 @@ public boolean acquireLock(String key, long timeout) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RegistryException("etcd get lock error", e); - } catch (ExecutionException e) { + } catch (Exception e) { throw new RegistryException("etcd get lock error, lockKey: " + key, e); } } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryProperties.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryProperties.java index babb6dea7637..b748c2a0fe93 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryProperties.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/main/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryProperties.java @@ -33,6 +33,8 @@ public class EtcdRegistryProperties { private String namespace = "dolphinscheduler"; private Duration connectionTimeout = Duration.ofSeconds(9); + private Duration ttl = Duration.ofSeconds(30); + // auth private String user; private String password; diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdKeepAliveLeaseManagerTest.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdKeepAliveLeaseManagerTest.java index eb716378d098..84acbae8f32c 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdKeepAliveLeaseManagerTest.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdKeepAliveLeaseManagerTest.java @@ -43,7 +43,7 @@ public static void before() throws Exception { .withNodes(1) .withImage("ibmcom/etcd:3.2.24") .build(); - server.restart(); + server.cluster().start(); client = Client.builder().endpoints(server.clientEndpoints()).build(); diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryTest.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryTest.java deleted file mode 100644 index b99bab98ad87..000000000000 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryTest.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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.registry.etcd; - -import org.apache.dolphinscheduler.registry.api.Event; -import org.apache.dolphinscheduler.registry.api.SubscribeListener; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.etcd.jetcd.test.EtcdClusterExtension; - -public class EtcdRegistryTest { - - private static final Logger logger = LoggerFactory.getLogger(EtcdRegistryTest.class); - - public static EtcdRegistry registry; - - @BeforeAll - public static void before() throws Exception { - EtcdClusterExtension server = EtcdClusterExtension.builder() - .withNodes(1) - .withImage("ibmcom/etcd:3.2.24") - .build(); - EtcdRegistryProperties properties = new EtcdRegistryProperties(); - server.restart(); - properties.setEndpoints(String.valueOf(server.clientEndpoints().get(0))); - registry = new EtcdRegistry(properties); - registry.put("/sub", "sub", false); - } - - @Test - public void persistTest() { - registry.put("/nodes/m1", "", false); - registry.put("/nodes/m2", "", false); - Assertions.assertEquals(Arrays.asList("m1", "m2"), registry.children("/nodes")); - Assertions.assertTrue(registry.exists("/nodes/m1")); - registry.delete("/nodes/m2"); - Assertions.assertFalse(registry.exists("/nodes/m2")); - registry.delete("/nodes"); - Assertions.assertFalse(registry.exists("/nodes/m1")); - } - - @Test - public void lockTest() { - CountDownLatch preCountDownLatch = new CountDownLatch(1); - CountDownLatch allCountDownLatch = new CountDownLatch(2); - List testData = new ArrayList<>(); - new Thread(() -> { - registry.acquireLock("/lock"); - preCountDownLatch.countDown(); - logger.info(Thread.currentThread().getName() - + " :I got the lock, but I don't want to work. I want to rest for a while"); - try { - Thread.sleep(1000); - logger.info(Thread.currentThread().getName() + " :I'm going to start working"); - testData.add("thread1"); - - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - logger.info(Thread.currentThread().getName() + " :I have finished my work, now I release the lock"); - registry.releaseLock("/lock"); - allCountDownLatch.countDown(); - } - }).start(); - try { - preCountDownLatch.await(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - new Thread(() -> { - try { - logger.info(Thread.currentThread().getName() + " :I am trying to acquire the lock"); - registry.acquireLock("/lock"); - logger.info(Thread.currentThread().getName() + " :I got the lock and I started working"); - - testData.add("thread2"); - } finally { - registry.releaseLock("/lock"); - allCountDownLatch.countDown(); - } - - }).start(); - try { - allCountDownLatch.await(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - Assertions.assertEquals(testData, Arrays.asList("thread1", "thread2")); - } - - @Test - public void subscribeTest() { - boolean status = registry.subscribe("/sub", new TestListener()); - // The following add and delete operations are used for debugging - registry.put("/sub/m1", "tt", false); - registry.put("/sub/m2", "tt", false); - registry.delete("/sub/m2"); - registry.delete("/sub"); - Assertions.assertTrue(status); - - } - - static class TestListener implements SubscribeListener { - - @Override - public void notify(Event event) { - logger.info("I'm test listener"); - } - } - - @AfterAll - public static void after() throws IOException { - registry.close(); - } -} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryTestCase.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryTestCase.java new file mode 100644 index 000000000000..1e751c18629d --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/java/org/apache/dolphinscheduler/plugin/registry/etcd/EtcdRegistryTestCase.java @@ -0,0 +1,70 @@ +/* + * 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.registry.etcd; + +import org.apache.dolphinscheduler.plugin.registry.RegistryTestCase; + +import java.net.URI; +import java.util.stream.Collectors; + +import lombok.SneakyThrows; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; + +import io.etcd.jetcd.launcher.EtcdCluster; +import io.etcd.jetcd.test.EtcdClusterExtension; + +@SpringBootTest(classes = EtcdRegistryProperties.class) +@SpringBootApplication(scanBasePackageClasses = EtcdRegistryProperties.class) +public class EtcdRegistryTestCase extends RegistryTestCase { + + @Autowired + private EtcdRegistryProperties etcdRegistryProperties; + + private static EtcdCluster etcdCluster; + + @SneakyThrows + @BeforeAll + public static void setUpTestingServer() { + etcdCluster = EtcdClusterExtension.builder() + .withNodes(1) + .withImage("ibmcom/etcd:3.2.24") + .build() + .cluster(); + etcdCluster.start(); + System.setProperty("registry.endpoints", + etcdCluster.clientEndpoints().stream().map(URI::toString).collect(Collectors.joining(","))); + } + + @SneakyThrows + @Override + public EtcdRegistry createRegistry() { + return new EtcdRegistry(etcdRegistryProperties); + } + + @SneakyThrows + @AfterAll + public static void tearDownTestingServer() { + try (EtcdCluster cluster = etcdCluster) { + } + } +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/resources/application.yaml b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/resources/application.yaml new file mode 100644 index 000000000000..083d38511c50 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/resources/application.yaml @@ -0,0 +1,20 @@ +# +# 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. +# + +registry: + type: etcd + ttl: 2s diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/resources/logback.xml b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/resources/logback.xml new file mode 100644 index 000000000000..6f211959c590 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-etcd/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-it/pom.xml b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-it/pom.xml new file mode 100644 index 000000000000..7f4b97d3efe5 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-it/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + org.apache.dolphinscheduler + dolphinscheduler-registry-plugins + dev-SNAPSHOT + + + dolphinscheduler-registry-it + + + + org.apache.dolphinscheduler + dolphinscheduler-registry-api + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + false + + + + + test-jar + + + + + + + + diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-it/src/test/java/org/apache/dolphinscheduler/plugin/registry/RegistryTestCase.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-it/src/test/java/org/apache/dolphinscheduler/plugin/registry/RegistryTestCase.java new file mode 100644 index 000000000000..8fbd6bc5c021 --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-it/src/test/java/org/apache/dolphinscheduler/plugin/registry/RegistryTestCase.java @@ -0,0 +1,290 @@ +/* + * 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.registry; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.dolphinscheduler.registry.api.ConnectionState; +import org.apache.dolphinscheduler.registry.api.Event; +import org.apache.dolphinscheduler.registry.api.Registry; +import org.apache.dolphinscheduler.registry.api.RegistryException; +import org.apache.dolphinscheduler.registry.api.SubscribeListener; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import lombok.SneakyThrows; + +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.google.common.truth.Truth; + +public abstract class RegistryTestCase { + + protected R registry; + + @BeforeEach + public void setupRegistry() { + registry = createRegistry(); + } + + @SneakyThrows + @AfterEach + public void tearDownRegistry() { + try (R registry = this.registry) { + } + } + + @Test + public void testIsConnected() { + registry.start(); + Truth.assertThat(registry.isConnected()).isTrue(); + } + + @Test + public void testConnectUntilTimeout() { + registry.start(); + await().atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> registry.connectUntilTimeout(Duration.ofSeconds(3))); + + } + + @SneakyThrows + @Test + public void testSubscribe() { + registry.start(); + + final AtomicBoolean subscribeAdded = new AtomicBoolean(false); + final AtomicBoolean subscribeRemoved = new AtomicBoolean(false); + final AtomicBoolean subscribeUpdated = new AtomicBoolean(false); + + SubscribeListener subscribeListener = event -> { + System.out.println("Receive event: " + event); + if (event.type() == Event.Type.ADD) { + subscribeAdded.compareAndSet(false, true); + } + if (event.type() == Event.Type.REMOVE) { + subscribeRemoved.compareAndSet(false, true); + } + if (event.type() == Event.Type.UPDATE) { + subscribeUpdated.compareAndSet(false, true); + } + }; + String key = "/nodes/master" + System.nanoTime(); + registry.subscribe(key, subscribeListener); + registry.put(key, String.valueOf(System.nanoTime()), true); + // Sleep 3 seconds here since in mysql jdbc registry + // If multiple event occurs in a refresh time, only the last event will be triggered + Thread.sleep(3000); + registry.put(key, String.valueOf(System.nanoTime()), true); + Thread.sleep(3000); + registry.delete(key); + + await().atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> { + Assertions.assertTrue(subscribeAdded.get()); + Assertions.assertTrue(subscribeUpdated.get()); + Assertions.assertTrue(subscribeRemoved.get()); + }); + } + + @SneakyThrows + @Test + public void testUnsubscribe() { + registry.start(); + + final AtomicBoolean subscribeAdded = new AtomicBoolean(false); + final AtomicBoolean subscribeRemoved = new AtomicBoolean(false); + final AtomicBoolean subscribeUpdated = new AtomicBoolean(false); + + SubscribeListener subscribeListener = event -> { + if (event.type() == Event.Type.ADD) { + subscribeAdded.compareAndSet(false, true); + } + if (event.type() == Event.Type.REMOVE) { + subscribeRemoved.compareAndSet(false, true); + } + if (event.type() == Event.Type.UPDATE) { + subscribeUpdated.compareAndSet(false, true); + } + }; + String key = "/nodes/master" + System.nanoTime(); + String value = "127.0.0.1:8080"; + registry.subscribe(key, subscribeListener); + registry.unsubscribe(key); + registry.put(key, value, true); + registry.put(key, value, true); + registry.delete(key); + + Thread.sleep(2000); + Assertions.assertFalse(subscribeAdded.get()); + Assertions.assertFalse(subscribeRemoved.get()); + Assertions.assertFalse(subscribeUpdated.get()); + + } + + @SneakyThrows + @Test + public void testAddConnectionStateListener() { + + AtomicReference connectionState = new AtomicReference<>(); + registry.addConnectionStateListener(connectionState::set); + + Truth.assertThat(connectionState.get()).isNull(); + registry.start(); + + await().atMost(Duration.ofSeconds(2)) + .until(() -> ConnectionState.CONNECTED == connectionState.get()); + + } + + @Test + public void testGet() { + registry.start(); + String key = "/nodes/master" + System.nanoTime(); + String value = "127.0.0.1:8080"; + assertThrows(RegistryException.class, () -> registry.get(key)); + registry.put(key, value, true); + Truth.assertThat(registry.get(key)).isEqualTo(value); + } + + @Test + public void testPut() { + registry.start(); + String key = "/nodes/master" + System.nanoTime(); + String value = "127.0.0.1:8080"; + registry.put(key, value, true); + Truth.assertThat(registry.get(key)).isEqualTo(value); + + // Update the value + registry.put(key, "123", true); + Truth.assertThat(registry.get(key)).isEqualTo("123"); + } + + @Test + public void testDelete() { + registry.start(); + String key = "/nodes/master" + System.nanoTime(); + String value = "127.0.0.1:8080"; + // Delete a non-existent key + registry.delete(key); + + registry.put(key, value, true); + Truth.assertThat(registry.get(key)).isEqualTo(value); + registry.delete(key); + Truth.assertThat(registry.exists(key)).isFalse(); + + } + + @Test + public void testChildren() { + registry.start(); + String master1 = "/nodes/children/127.0.0.1:8080"; + String master2 = "/nodes/children/127.0.0.2:8080"; + String value = "123"; + registry.put(master1, value, true); + registry.put(master2, value, true); + Truth.assertThat(registry.children("/nodes/children")) + .containsAtLeastElementsIn(Lists.newArrayList("127.0.0.1:8080", "127.0.0.2:8080")); + } + + @Test + public void testExists() { + registry.start(); + String key = "/nodes/master" + System.nanoTime(); + String value = "123"; + Truth.assertThat(registry.exists(key)).isFalse(); + registry.put(key, value, true); + Truth.assertThat(registry.exists(key)).isTrue(); + + } + + @SneakyThrows + @Test + public void testAcquireLock() { + registry.start(); + String lockKey = "/lock" + System.nanoTime(); + + // 1. Acquire the lock at the main thread + Truth.assertThat(registry.acquireLock(lockKey)).isTrue(); + // Acquire the lock at the main thread again + // It should acquire success + Truth.assertThat(registry.acquireLock(lockKey)).isTrue(); + + // Acquire the lock at another thread + // It should acquire failed + CompletableFuture acquireResult = CompletableFuture.supplyAsync(() -> registry.acquireLock(lockKey)); + assertThrows(TimeoutException.class, () -> acquireResult.get(3000, TimeUnit.MILLISECONDS)); + + } + + @SneakyThrows + @Test + public void testAcquireLock_withTimeout() { + registry.start(); + String lockKey = "/lock" + System.nanoTime(); + // 1. Acquire the lock in the main thread + Truth.assertThat(registry.acquireLock(lockKey, 3000)).isTrue(); + + // Acquire the lock in the main thread + // It should acquire success + Truth.assertThat(registry.acquireLock(lockKey, 3000)).isTrue(); + + // Acquire the lock at another thread + // It should acquire failed + CompletableFuture acquireResult = + CompletableFuture.supplyAsync(() -> registry.acquireLock(lockKey, 3000)); + Truth.assertThat(acquireResult.get()).isFalse(); + + } + + @SneakyThrows + @Test + public void testReleaseLock() { + registry.start(); + String lockKey = "/lock" + System.nanoTime(); + // 1. Acquire the lock in the main thread + Truth.assertThat(registry.acquireLock(lockKey, 3000)).isTrue(); + + // Acquire the lock at another thread + // It should acquire failed + CompletableFuture acquireResult = + CompletableFuture.supplyAsync(() -> registry.acquireLock(lockKey, 3000)); + Truth.assertThat(acquireResult.get()).isFalse(); + + // 2. Release the lock in the main thread + Truth.assertThat(registry.releaseLock(lockKey)).isTrue(); + + // Acquire the lock at another thread + // It should acquire success + acquireResult = CompletableFuture.supplyAsync(() -> registry.acquireLock(lockKey, 3000)); + Truth.assertThat(acquireResult.get()).isTrue(); + } + + public abstract R createRegistry(); + +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/pom.xml b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/pom.xml index d4285edfbdf4..aa592b9da450 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/pom.xml +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/pom.xml @@ -72,6 +72,31 @@ + + + org.apache.dolphinscheduler + dolphinscheduler-registry-it + ${project.version} + test-jar + test + + + + org.testcontainers + mysql + + + + org.testcontainers + postgresql + + + + org.springframework.boot + spring-boot-starter-test + test + + diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/EphemeralDateManager.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/EphemeralDateManager.java similarity index 93% rename from dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/EphemeralDateManager.java rename to dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/EphemeralDateManager.java index 64915e8ca894..7c601b91a1f9 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/EphemeralDateManager.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/EphemeralDateManager.java @@ -15,12 +15,10 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.plugin.registry.jdbc.task; +package org.apache.dolphinscheduler.plugin.registry.jdbc; import static com.google.common.base.Preconditions.checkNotNull; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcOperator; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryProperties; import org.apache.dolphinscheduler.registry.api.ConnectionListener; import org.apache.dolphinscheduler.registry.api.ConnectionState; @@ -42,7 +40,7 @@ * This thread is used to check the connect state to jdbc. */ @Slf4j -public class EphemeralDateManager implements AutoCloseable { +class EphemeralDateManager implements AutoCloseable { private ConnectionState connectionState; private final JdbcOperator jdbcOperator; @@ -51,7 +49,7 @@ public class EphemeralDateManager implements AutoCloseable { private final Set ephemeralDateIds = Collections.synchronizedSet(new HashSet<>()); private final ScheduledExecutorService scheduledExecutorService; - public EphemeralDateManager(JdbcRegistryProperties registryProperties, JdbcOperator jdbcOperator) { + EphemeralDateManager(JdbcRegistryProperties registryProperties, JdbcOperator jdbcOperator) { this.registryProperties = registryProperties; this.jdbcOperator = checkNotNull(jdbcOperator); this.scheduledExecutorService = Executors.newScheduledThreadPool( @@ -151,7 +149,7 @@ private ConnectionState getConnectionState() { } } - private void updateEphemeralDateTerm() throws SQLException { + private void updateEphemeralDateTerm() { if (!jdbcOperator.updateEphemeralDataTerm(ephemeralDateIds)) { log.warn("Update jdbc registry ephemeral data: {} term error", ephemeralDateIds); } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcOperator.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcOperator.java index a56d609da77c..95f58a4a2098 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcOperator.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcOperator.java @@ -29,26 +29,25 @@ import java.sql.SQLException; import java.sql.SQLIntegrityConstraintViolationException; import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; +import org.springframework.dao.DuplicateKeyException; -@Component -@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "jdbc") -public class JdbcOperator { +public final class JdbcOperator { - @Autowired - private JdbcRegistryDataMapper jdbcRegistryDataMapper; - @Autowired - private JdbcRegistryLockMapper jdbcRegistryLockMapper; + private final JdbcRegistryDataMapper jdbcRegistryDataMapper; + private final JdbcRegistryLockMapper jdbcRegistryLockMapper; private final long expireTimeWindow; - public JdbcOperator(JdbcRegistryProperties registryProperties) { + JdbcOperator(JdbcRegistryProperties registryProperties, + JdbcRegistryDataMapper jdbcRegistryDataMapper, + JdbcRegistryLockMapper jdbcRegistryLockMapper) { this.expireTimeWindow = registryProperties.getTermExpireTimes() * registryProperties.getTermRefreshInterval().toMillis(); + this.jdbcRegistryDataMapper = jdbcRegistryDataMapper; + this.jdbcRegistryLockMapper = jdbcRegistryLockMapper; } public void healthCheck() { @@ -62,17 +61,21 @@ public List queryAllJdbcRegistryData() { public Long insertOrUpdateEphemeralData(String key, String value) throws SQLException { JdbcRegistryData jdbcRegistryData = jdbcRegistryDataMapper.selectByKey(key); if (jdbcRegistryData != null) { - long id = jdbcRegistryData.getId(); - if (jdbcRegistryDataMapper.updateDataAndTermById(id, value, System.currentTimeMillis()) <= 0) { + jdbcRegistryData.setDataValue(value); + jdbcRegistryData.setLastUpdateTime(new Date()); + jdbcRegistryData.setLastTerm(System.currentTimeMillis()); + if (jdbcRegistryDataMapper.updateById(jdbcRegistryData) <= 0) { throw new SQLException(String.format("update registry value failed, key: %s, value: %s", key, value)); } - return id; + return jdbcRegistryData.getId(); } jdbcRegistryData = JdbcRegistryData.builder() .dataKey(key) .dataValue(value) .dataType(DataType.EPHEMERAL.getTypeValue()) .lastTerm(System.currentTimeMillis()) + .lastUpdateTime(new Date()) + .createTime(new Date()) .build(); jdbcRegistryDataMapper.insert(jdbcRegistryData); return jdbcRegistryData.getId(); @@ -81,17 +84,21 @@ public Long insertOrUpdateEphemeralData(String key, String value) throws SQLExce public long insertOrUpdatePersistentData(String key, String value) throws SQLException { JdbcRegistryData jdbcRegistryData = jdbcRegistryDataMapper.selectByKey(key); if (jdbcRegistryData != null) { - long id = jdbcRegistryData.getId(); - if (jdbcRegistryDataMapper.updateDataAndTermById(id, value, System.currentTimeMillis()) <= 0) { + jdbcRegistryData.setDataValue(value); + jdbcRegistryData.setLastUpdateTime(new Date()); + jdbcRegistryData.setLastTerm(System.currentTimeMillis()); + if (jdbcRegistryDataMapper.updateById(jdbcRegistryData) <= 0) { throw new SQLException(String.format("update registry value failed, key: %s, value: %s", key, value)); } - return id; + return jdbcRegistryData.getId(); } jdbcRegistryData = JdbcRegistryData.builder() .dataKey(key) .dataValue(value) .dataType(DataType.PERSISTENT.getTypeValue()) .lastTerm(System.currentTimeMillis()) + .lastUpdateTime(new Date()) + .createTime(new Date()) .build(); jdbcRegistryDataMapper.insert(jdbcRegistryData); return jdbcRegistryData.getId(); @@ -127,7 +134,7 @@ public List getChildren(String key) throws SQLException { .collect(Collectors.toList()); } - public boolean existKey(String key) throws SQLException { + public boolean existKey(String key) { JdbcRegistryData jdbcRegistryData = jdbcRegistryDataMapper.selectByKey(key); return jdbcRegistryData != null; } @@ -136,24 +143,25 @@ public boolean existKey(String key) throws SQLException { * Try to acquire the target Lock, if cannot acquire, return null. */ @SuppressWarnings("checkstyle:IllegalCatch") - public JdbcRegistryLock tryToAcquireLock(String key) throws SQLException { + public JdbcRegistryLock tryToAcquireLock(String key) { JdbcRegistryLock jdbcRegistryLock = JdbcRegistryLock.builder() .lockKey(key) - .lockOwner(JdbcRegistryConstant.LOCK_OWNER) + .lockOwner(LockUtils.getLockOwner()) .lastTerm(System.currentTimeMillis()) + .lastUpdateTime(new Date()) .build(); try { jdbcRegistryLockMapper.insert(jdbcRegistryLock); return jdbcRegistryLock; } catch (Exception e) { - if (e instanceof SQLIntegrityConstraintViolationException) { + if (e instanceof SQLIntegrityConstraintViolationException || e instanceof DuplicateKeyException) { return null; } throw e; } } - public JdbcRegistryLock getLockById(long lockId) throws SQLException { + public JdbcRegistryLock getLockById(long lockId) { return jdbcRegistryLockMapper.selectById(lockId); } @@ -161,7 +169,7 @@ public boolean releaseLock(long lockId) throws SQLException { return jdbcRegistryLockMapper.deleteById(lockId) > 0; } - public boolean updateEphemeralDataTerm(Collection ephemeralDateIds) throws SQLException { + public boolean updateEphemeralDataTerm(Collection ephemeralDateIds) { if (CollectionUtils.isEmpty(ephemeralDateIds)) { return true; } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistry.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistry.java index 12b29c34cc1d..2b7993c87bdc 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistry.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistry.java @@ -17,9 +17,7 @@ package org.apache.dolphinscheduler.plugin.registry.jdbc; -import org.apache.dolphinscheduler.plugin.registry.jdbc.task.EphemeralDateManager; -import org.apache.dolphinscheduler.plugin.registry.jdbc.task.RegistryLockManager; -import org.apache.dolphinscheduler.plugin.registry.jdbc.task.SubscribeDataManager; +import org.apache.dolphinscheduler.plugin.registry.jdbc.model.JdbcRegistryData; import org.apache.dolphinscheduler.registry.api.ConnectionListener; import org.apache.dolphinscheduler.registry.api.ConnectionState; import org.apache.dolphinscheduler.registry.api.Registry; @@ -30,31 +28,24 @@ import java.time.Duration; import java.util.Collection; -import javax.annotation.PostConstruct; - import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - /** * This is one of the implementation of {@link Registry}, with this implementation, you need to rely on mysql database to * store the DolphinScheduler master/worker's metadata and do the server registry/unRegistry. */ -@Component -@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "jdbc") @Slf4j -public class JdbcRegistry implements Registry { +public final class JdbcRegistry implements Registry { private final JdbcRegistryProperties jdbcRegistryProperties; private final EphemeralDateManager ephemeralDateManager; private final SubscribeDataManager subscribeDataManager; private final RegistryLockManager registryLockManager; - private JdbcOperator jdbcOperator; + private final JdbcOperator jdbcOperator; - public JdbcRegistry(JdbcRegistryProperties jdbcRegistryProperties, - JdbcOperator jdbcOperator) { + JdbcRegistry(JdbcRegistryProperties jdbcRegistryProperties, + JdbcOperator jdbcOperator) { this.jdbcOperator = jdbcOperator; jdbcOperator.clearExpireLock(); jdbcOperator.clearExpireEphemeralDate(); @@ -65,7 +56,7 @@ public JdbcRegistry(JdbcRegistryProperties jdbcRegistryProperties, log.info("Initialize Jdbc Registry..."); } - @PostConstruct + @Override public void start() { log.info("Starting Jdbc Registry..."); // start a jdbc connect check @@ -103,10 +94,9 @@ public void connectUntilTimeout(@NonNull Duration timeout) throws RegistryExcept } @Override - public boolean subscribe(String path, SubscribeListener listener) { + public void subscribe(String path, SubscribeListener listener) { // new a schedule thread to query the path, if the path subscribeDataManager.addListener(path, listener); - return true; } @Override @@ -122,8 +112,18 @@ public void addConnectionStateListener(ConnectionListener listener) { @Override public String get(String key) { - // get the key value - return subscribeDataManager.getData(key); + try { + // get the key value + JdbcRegistryData data = jdbcOperator.getData(key); + if (data == null) { + throw new RegistryException("key: " + key + " not exist"); + } + return data.getDataValue(); + } catch (RegistryException registryException) { + throw registryException; + } catch (Exception e) { + throw new RegistryException(String.format("Get key: %s error", key), e); + } } @Override diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java index f21ce0d67caa..603a47632216 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryAutoConfiguration.java @@ -49,6 +49,18 @@ public JdbcRegistryAutoConfiguration() { log.info("Load JdbcRegistryAutoConfiguration"); } + @Bean + public JdbcOperator jdbcOperator(JdbcRegistryProperties jdbcRegistryProperties, + JdbcRegistryDataMapper jdbcRegistryDataMapper, + JdbcRegistryLockMapper jdbcRegistryLockMapper) { + return new JdbcOperator(jdbcRegistryProperties, jdbcRegistryDataMapper, jdbcRegistryLockMapper); + } + + @Bean + public JdbcRegistry jdbcRegistry(JdbcRegistryProperties jdbcRegistryProperties, JdbcOperator jdbcOperator) { + return new JdbcRegistry(jdbcRegistryProperties, jdbcOperator); + } + @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(JdbcRegistryProperties jdbcRegistryProperties) throws Exception { diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryConstant.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryConstant.java index 4a016f4d2ede..84496afb809b 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryConstant.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/JdbcRegistryConstant.java @@ -17,15 +17,11 @@ package org.apache.dolphinscheduler.plugin.registry.jdbc; -import org.apache.dolphinscheduler.common.utils.NetUtils; -import org.apache.dolphinscheduler.common.utils.OSUtils; - import lombok.experimental.UtilityClass; @UtilityClass -public final class JdbcRegistryConstant { +final class JdbcRegistryConstant { public static final long LOCK_ACQUIRE_INTERVAL = 1_000; - public static final String LOCK_OWNER = NetUtils.getHost() + "_" + OSUtils.getProcessID(); } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/LockUtils.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/LockUtils.java new file mode 100644 index 000000000000..f70f0afa5b0f --- /dev/null +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/LockUtils.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.dolphinscheduler.plugin.registry.jdbc; + +import org.apache.dolphinscheduler.common.utils.NetUtils; +import org.apache.dolphinscheduler.common.utils.OSUtils; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class LockUtils { + + private static final String LOCK_OWNER_PREFIX = NetUtils.getHost() + "_" + OSUtils.getProcessID() + "_"; + + public static String getLockOwner() { + return LOCK_OWNER_PREFIX + Thread.currentThread().getName(); + } + +} diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/RegistryLockManager.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/RegistryLockManager.java similarity index 73% rename from dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/RegistryLockManager.java rename to dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/RegistryLockManager.java index b624b9e78828..6c519685ff45 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/RegistryLockManager.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/RegistryLockManager.java @@ -15,12 +15,9 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.plugin.registry.jdbc.task; +package org.apache.dolphinscheduler.plugin.registry.jdbc; import org.apache.dolphinscheduler.common.thread.ThreadUtils; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcOperator; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryConstant; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryProperties; import org.apache.dolphinscheduler.plugin.registry.jdbc.model.JdbcRegistryLock; import org.apache.dolphinscheduler.registry.api.RegistryException; @@ -40,14 +37,15 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; @Slf4j -public class RegistryLockManager implements AutoCloseable { +class RegistryLockManager implements AutoCloseable { private final JdbcOperator jdbcOperator; private final JdbcRegistryProperties registryProperties; + // lock owner -> lock private final Map lockHoldMap; private final ScheduledExecutorService lockTermUpdateThreadPool; - public RegistryLockManager(JdbcRegistryProperties registryProperties, JdbcOperator jdbcOperator) { + RegistryLockManager(JdbcRegistryProperties registryProperties, JdbcOperator jdbcOperator) { this.registryProperties = registryProperties; this.jdbcOperator = jdbcOperator; this.lockHoldMap = new ConcurrentHashMap<>(); @@ -67,20 +65,24 @@ public void start() { * Acquire the lock, if cannot get the lock will await. */ public void acquireLock(String lockKey) throws RegistryException { - // maybe we can use the computeIf absent - lockHoldMap.computeIfAbsent(lockKey, key -> { - JdbcRegistryLock jdbcRegistryLock; - try { - while ((jdbcRegistryLock = jdbcOperator.tryToAcquireLock(lockKey)) == null) { - log.debug("Acquire the lock {} failed try again", key); - // acquire failed, wait and try again - ThreadUtils.sleep(JdbcRegistryConstant.LOCK_ACQUIRE_INTERVAL); + try { + while (true) { + JdbcRegistryLock jdbcRegistryLock = lockHoldMap.get(lockKey); + if (jdbcRegistryLock != null && LockUtils.getLockOwner().equals(jdbcRegistryLock.getLockOwner())) { + return; } - } catch (SQLException e) { - throw new RegistryException("Acquire the lock error", e); + jdbcRegistryLock = jdbcOperator.tryToAcquireLock(lockKey); + if (jdbcRegistryLock != null) { + lockHoldMap.put(lockKey, jdbcRegistryLock); + return; + } + log.debug("Acquire the lock {} failed try again", lockKey); + // acquire failed, wait and try again + ThreadUtils.sleep(JdbcRegistryConstant.LOCK_ACQUIRE_INTERVAL); } - return jdbcRegistryLock; - }); + } catch (Exception ex) { + throw new RegistryException("Acquire the lock: " + lockKey + " error", ex); + } } /** @@ -88,21 +90,22 @@ public void acquireLock(String lockKey) throws RegistryException { */ public boolean acquireLock(String lockKey, long timeout) throws RegistryException { long startTime = System.currentTimeMillis(); - while (System.currentTimeMillis() - startTime < timeout) { - try { - if (lockHoldMap.containsKey(lockKey)) { + try { + while (System.currentTimeMillis() - startTime < timeout) { + JdbcRegistryLock jdbcRegistryLock = lockHoldMap.get(lockKey); + if (jdbcRegistryLock != null && LockUtils.getLockOwner().equals(jdbcRegistryLock.getLockOwner())) { return true; } - JdbcRegistryLock jdbcRegistryLock = jdbcOperator.tryToAcquireLock(lockKey); + jdbcRegistryLock = jdbcOperator.tryToAcquireLock(lockKey); if (jdbcRegistryLock != null) { lockHoldMap.put(lockKey, jdbcRegistryLock); return true; } - } catch (SQLException e) { - throw new RegistryException("Acquire the lock: " + lockKey + " error", e); + log.debug("Acquire the lock {} failed try again", lockKey); + ThreadUtils.sleep(JdbcRegistryConstant.LOCK_ACQUIRE_INTERVAL); } - log.debug("Acquire the lock {} failed try again", lockKey); - ThreadUtils.sleep(JdbcRegistryConstant.LOCK_ACQUIRE_INTERVAL); + } catch (Exception e) { + throw new RegistryException("Acquire the lock: " + lockKey + " error", e); } return false; } @@ -115,6 +118,7 @@ public void releaseLock(String lockKey) { jdbcOperator.releaseLock(jdbcRegistryLock.getId()); lockHoldMap.remove(lockKey); } catch (SQLException e) { + lockHoldMap.remove(lockKey); throw new RegistryException(String.format("Release lock: %s error", lockKey), e); } } @@ -149,7 +153,6 @@ public void run() { if (!jdbcOperator.updateLockTerm(lockIds)) { log.warn("Update the lock: {} term failed.", lockIds); } - jdbcOperator.clearExpireLock(); } catch (Exception e) { log.error("Update lock term error", e); } diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/SubscribeDataManager.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/SubscribeDataManager.java similarity index 91% rename from dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/SubscribeDataManager.java rename to dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/SubscribeDataManager.java index 4718b053f47e..e86dc4b15585 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/task/SubscribeDataManager.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/SubscribeDataManager.java @@ -15,10 +15,8 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.plugin.registry.jdbc.task; +package org.apache.dolphinscheduler.plugin.registry.jdbc; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcOperator; -import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryProperties; import org.apache.dolphinscheduler.plugin.registry.jdbc.model.JdbcRegistryData; import org.apache.dolphinscheduler.registry.api.Event; import org.apache.dolphinscheduler.registry.api.SubscribeListener; @@ -42,7 +40,7 @@ * Used to refresh if the subscribe path has been changed. */ @Slf4j -public class SubscribeDataManager implements AutoCloseable { +class SubscribeDataManager implements AutoCloseable { private final JdbcOperator jdbcOperator; private final JdbcRegistryProperties registryProperties; @@ -50,7 +48,7 @@ public class SubscribeDataManager implements AutoCloseable { private final ScheduledExecutorService dataSubscribeCheckThreadPool; private final Map jdbcRegistryDataMap = new ConcurrentHashMap<>(); - public SubscribeDataManager(JdbcRegistryProperties registryProperties, JdbcOperator jdbcOperator) { + SubscribeDataManager(JdbcRegistryProperties registryProperties, JdbcOperator jdbcOperator) { this.registryProperties = registryProperties; this.jdbcOperator = jdbcOperator; this.dataSubscribeCheckThreadPool = Executors.newScheduledThreadPool( @@ -75,12 +73,8 @@ public void removeListener(String path) { dataSubScribeMap.remove(path); } - public String getData(String path) { - JdbcRegistryData jdbcRegistryData = jdbcRegistryDataMap.get(path); - if (jdbcRegistryData == null) { - return null; - } - return jdbcRegistryData.getDataValue(); + public JdbcRegistryData getData(String path) { + return jdbcRegistryDataMap.get(path); } @Override @@ -107,6 +101,7 @@ public void run() { List addedData = new ArrayList<>(); List deletedData = new ArrayList<>(); List updatedData = new ArrayList<>(); + for (Map.Entry entry : currentJdbcDataMap.entrySet()) { JdbcRegistryData newData = entry.getValue(); JdbcRegistryData oldData = jdbcRegistryDataMap.get(entry.getKey()); @@ -118,6 +113,7 @@ public void run() { } } } + for (Map.Entry entry : jdbcRegistryDataMap.entrySet()) { if (!currentJdbcDataMap.containsKey(entry.getKey())) { deletedData.add(entry.getValue()); diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/mapper/JdbcRegistryDataMapper.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/mapper/JdbcRegistryDataMapper.java index 701f2e7310b5..e1d27bbf0b74 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/mapper/JdbcRegistryDataMapper.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/mapper/JdbcRegistryDataMapper.java @@ -40,9 +40,6 @@ public interface JdbcRegistryDataMapper extends BaseMapper { @Select("select * from t_ds_jdbc_registry_data where data_key like CONCAT (#{key}, '%')") List fuzzyQueryByKey(@Param("key") String key); - @Update("update t_ds_jdbc_registry_data set data_value = #{data}, last_term = #{term} where id = #{id}") - int updateDataAndTermById(@Param("id") long id, @Param("data") String data, @Param("term") long term); - @Delete("delete from t_ds_jdbc_registry_data where data_key = #{key}") void deleteByKey(@Param("key") String key); diff --git a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/mapper/JdbcRegistryLockMapper.java b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/mapper/JdbcRegistryLockMapper.java index 2d11c90a2414..0f529a878670 100644 --- a/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/mapper/JdbcRegistryLockMapper.java +++ b/dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/mapper/JdbcRegistryLockMapper.java @@ -38,7 +38,7 @@ public interface JdbcRegistryLockMapper extends BaseMapper { @Update({"

~{R?5Skz(}N>bmx%CM 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 VbxO_ZTvz}A002ovPDHLkV1g}g3uyoV literal 221254 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�_U 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