diff --git a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/java/org/apache/dolphinscheduler/api/test/cases/SchedulerAPITest.java b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/java/org/apache/dolphinscheduler/api/test/cases/SchedulerAPITest.java index 2919b0a66f08..e7250ac9f459 100644 --- a/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/java/org/apache/dolphinscheduler/api/test/cases/SchedulerAPITest.java +++ b/dolphinscheduler-api-test/dolphinscheduler-api-test-case/src/test/java/org/apache/dolphinscheduler/api/test/cases/SchedulerAPITest.java @@ -111,7 +111,7 @@ public void testCreateSchedule() { workflowDefinitionPage.releaseWorkflowDefinition(loginUser, projectCode, workflowDefinitionCode, ReleaseState.ONLINE); final String schedule = - "{\"startTime\":\"2019-08-08 00:00:00\",\"endTime\":\"2100-08-08 00:00:00\",\"timezoneId\":\"America/Phoenix\",\"crontab\":\"0 0 3/6 * * ? *\"}"; + "{\"startTime\":\"2019-08-08 00:00:00\",\"endTime\":\"2100-08-08 00:00:00\",\"timezoneId\":\"America/Phoenix\",\"triggerType\":\"INTERVAL\",\"crontab\":\"{\\\"hour\\\":0,\\\"minute\\\":0,\\\"second\\\":10,\\\"repeat\\\":2}\"}"; HttpResponse createScheduleResponse = schedulerPage.createSchedule(loginUser, projectCode, workflowDefinitionCode, schedule); Assertions.assertTrue(createScheduleResponse.getBody().getSuccess()); @@ -124,6 +124,8 @@ public void testQueryScheduleList() { HttpResponse queryScheduleListResponse = schedulerPage.queryScheduleList(loginUser, projectCode); Assertions.assertTrue(queryScheduleListResponse.getBody().getSuccess()); Assertions.assertTrue(queryScheduleListResponse.getBody().getData().toString().contains("2019-08-08")); + Assertions.assertTrue( + queryScheduleListResponse.getBody().getData().toString().contains("triggerType=INTERVAL")); scheduleId = (int) ((LinkedHashMap) ((List) queryScheduleListResponse.getBody() .getData()).get(0)).get("id"); } @@ -156,7 +158,7 @@ public void testOfflineSchedule() { @Order(5) public void testUpdateSchedule() { final String schedule = - "{\"startTime\":\"1996-08-08 00:00:00\",\"endTime\":\"2200-08-08 00:00:00\",\"timezoneId\":\"America/Phoenix\",\"crontab\":\"0 0 3/6 * * ? *\"}"; + "{\"startTime\":\"1996-08-08 00:00:00\",\"endTime\":\"2200-08-08 00:00:00\",\"timezoneId\":\"America/Phoenix\",\"triggerType\":\"INTERVAL\",\"crontab\":\"{\\\"hour\\\":0,\\\"minute\\\":0,\\\"second\\\":20,\\\"repeat\\\":1}\"}"; HttpResponse updateScheduleResponse = schedulerPage.updateSchedule(loginUser, projectCode, scheduleId, schedule); Assertions.assertTrue(updateScheduleResponse.getBody().getSuccess()); @@ -164,6 +166,8 @@ public void testUpdateSchedule() { HttpResponse queryScheduleListResponse = schedulerPage.queryScheduleList(loginUser, projectCode); Assertions.assertTrue(queryScheduleListResponse.getBody().getSuccess()); Assertions.assertTrue(queryScheduleListResponse.getBody().getData().toString().contains("1996-08-08")); + Assertions.assertTrue( + queryScheduleListResponse.getBody().getData().toString().contains("triggerType=INTERVAL")); } @Test diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/ScheduleParam.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/ScheduleParam.java index 7d70c77b9c3a..a5154f2ee5b8 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/ScheduleParam.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/ScheduleParam.java @@ -18,6 +18,7 @@ package org.apache.dolphinscheduler.api.dto; import org.apache.dolphinscheduler.common.enums.ScheduleMissedFirePolicy; +import org.apache.dolphinscheduler.common.enums.ScheduleTriggerType; import java.util.Date; @@ -36,10 +37,14 @@ public class ScheduleParam { private String crontab; private String timezoneId; private ScheduleMissedFirePolicy missedFirePolicy = ScheduleMissedFirePolicy.FIRE_ALL_MISSED; + private ScheduleTriggerType triggerType = ScheduleTriggerType.CRON; @JsonIgnore private boolean missedFirePolicySet; + @JsonIgnore + private boolean triggerTypeSet; + public ScheduleParam() { } @@ -59,6 +64,15 @@ public boolean isMissedFirePolicySet() { return missedFirePolicySet; } + public void setTriggerType(ScheduleTriggerType triggerType) { + this.triggerType = triggerType; + this.triggerTypeSet = true; + } + + public boolean isTriggerTypeSet() { + return triggerTypeSet; + } + @Override public String toString() { return "ScheduleParam{" diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/SchedulerServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/SchedulerServiceImpl.java index 7b0ae201c947..69a5eacdb2fc 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/SchedulerServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/SchedulerServiceImpl.java @@ -34,9 +34,11 @@ import org.apache.dolphinscheduler.common.enums.FailureStrategy; import org.apache.dolphinscheduler.common.enums.Priority; import org.apache.dolphinscheduler.common.enums.ReleaseState; +import org.apache.dolphinscheduler.common.enums.ScheduleTriggerType; import org.apache.dolphinscheduler.common.enums.UserType; import org.apache.dolphinscheduler.common.enums.WarningType; import org.apache.dolphinscheduler.common.utils.DateUtils; +import org.apache.dolphinscheduler.common.utils.IntervalSchedule; import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.dolphinscheduler.dao.entity.Project; import org.apache.dolphinscheduler.dao.entity.Schedule; @@ -52,6 +54,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -167,13 +170,16 @@ public Schedule insertSchedule(User loginUser, scheduleObj.setStartTime(scheduleParam.getStartTime()); scheduleObj.setEndTime(scheduleParam.getEndTime()); - if (!CronUtils.isValidExpression(scheduleParam.getCrontab())) { + if (!isValidScheduleExpression(scheduleParam)) { log.error("Schedule crontab verify failure, crontab:{}.", scheduleParam.getCrontab()); throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, scheduleParam.getCrontab()); } scheduleObj.setCrontab(scheduleParam.getCrontab()); validateMissedFirePolicy(scheduleParam); + validateTriggerType(scheduleParam); scheduleObj.setMissedFirePolicy(scheduleParam.getMissedFirePolicy()); + scheduleObj.setTriggerType( + scheduleParam.getTriggerType() == null ? ScheduleTriggerType.CRON : scheduleParam.getTriggerType()); scheduleObj.setTimezoneId(scheduleParam.getTimezoneId()); scheduleObj.setWarningType(warningType); scheduleObj.setWarningGroupId(warningGroupId); @@ -389,6 +395,20 @@ public List previewSchedule(User loginUser, String schedule) { ZonedDateTime startTime = ZonedDateTime.ofInstant(scheduleParam.getStartTime().toInstant(), zoneId); ZonedDateTime endTime = ZonedDateTime.ofInstant(scheduleParam.getEndTime().toInstant(), zoneId); startTime = now.isAfter(startTime) ? now : startTime; + if (scheduleParam.getTriggerType() == ScheduleTriggerType.INTERVAL) { + IntervalSchedule intervalSchedule = IntervalSchedule.parse(scheduleParam.getCrontab()); + List fireTimes = new ArrayList<>(); + int executionLimit = intervalSchedule.getRepeatCount() < 0 + ? Constants.PREVIEW_SCHEDULE_EXECUTE_COUNT + : Math.min(intervalSchedule.getRepeatCount() + 1, Constants.PREVIEW_SCHEDULE_EXECUTE_COUNT); + for (int i = 0; i < executionLimit && !startTime.isAfter(endTime); i++) { + fireTimes.add(startTime); + startTime = startTime.plus(Duration.ofMillis(intervalSchedule.getIntervalMilliseconds())); + } + return fireTimes.stream() + .map(t -> DateUtils.dateToString(t, zoneId)) + .collect(Collectors.toList()); + } try { cron = CronUtils.parse2Cron(scheduleParam.getCrontab()); @@ -403,6 +423,24 @@ public List previewSchedule(User loginUser, String schedule) { .collect(Collectors.toList()); } + private boolean isValidScheduleExpression(ScheduleParam scheduleParam) { + ScheduleTriggerType triggerType = scheduleParam.getTriggerType() == null + ? ScheduleTriggerType.CRON + : scheduleParam.getTriggerType(); + return isValidScheduleExpression(scheduleParam, triggerType); + } + + private boolean isValidScheduleExpression(ScheduleParam scheduleParam, ScheduleTriggerType triggerType) { + if (triggerType == ScheduleTriggerType.CRON) { + return CronUtils.isValidExpression(scheduleParam.getCrontab()); + } + try { + IntervalSchedule.parse(scheduleParam.getCrontab()); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } /** * update workflow definition schedule * @@ -554,15 +592,26 @@ private Schedule updateSchedule(Schedule schedule, WorkflowDefinition workflowDe schedule.setStartTime(scheduleParam.getStartTime()); schedule.setEndTime(scheduleParam.getEndTime()); - if (!CronUtils.isValidExpression(scheduleParam.getCrontab())) { - log.error("Schedule crontab verify failure, crontab:{}.", scheduleParam.getCrontab()); + ScheduleTriggerType effectiveTriggerType = schedule.getTriggerType() == null + ? ScheduleTriggerType.CRON + : schedule.getTriggerType(); + if (scheduleParam.isTriggerTypeSet() && scheduleParam.getTriggerType() != null) { + effectiveTriggerType = scheduleParam.getTriggerType(); + } + if (!isValidScheduleExpression(scheduleParam, effectiveTriggerType)) { + log.error("Schedule expression validation failure, triggerType:{}, expression:{}.", + effectiveTriggerType, scheduleParam.getCrontab()); throw new ServiceException(Status.SCHEDULE_CRON_CHECK_FAILED, scheduleParam.getCrontab()); } schedule.setCrontab(scheduleParam.getCrontab()); validateMissedFirePolicy(scheduleParam); + validateTriggerType(scheduleParam); if (scheduleParam.isMissedFirePolicySet() && scheduleParam.getMissedFirePolicy() != null) { schedule.setMissedFirePolicy(scheduleParam.getMissedFirePolicy()); } + if (scheduleParam.isTriggerTypeSet() && scheduleParam.getTriggerType() != null) { + schedule.setTriggerType(scheduleParam.getTriggerType()); + } schedule.setTimezoneId(scheduleParam.getTimezoneId()); } @@ -598,4 +647,11 @@ private void validateMissedFirePolicy(ScheduleParam scheduleParam) { } } + private void validateTriggerType(ScheduleParam scheduleParam) { + if (scheduleParam.isTriggerTypeSet() && scheduleParam.getTriggerType() == null) { + log.warn("Schedule trigger type is invalid."); + throw new ServiceException(Status.REQUEST_PARAMS_NOT_VALID_ERROR, "triggerType"); + } + } + } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/vo/ScheduleVO.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/vo/ScheduleVO.java index 8ddf9c3ade64..065af206430e 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/vo/ScheduleVO.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/vo/ScheduleVO.java @@ -21,6 +21,7 @@ import org.apache.dolphinscheduler.common.enums.Priority; import org.apache.dolphinscheduler.common.enums.ReleaseState; import org.apache.dolphinscheduler.common.enums.ScheduleMissedFirePolicy; +import org.apache.dolphinscheduler.common.enums.ScheduleTriggerType; import org.apache.dolphinscheduler.common.enums.WarningType; import org.apache.dolphinscheduler.common.utils.DateUtils; import org.apache.dolphinscheduler.dao.entity.Schedule; @@ -57,6 +58,8 @@ public class ScheduleVO { private ScheduleMissedFirePolicy missedFirePolicy; + private ScheduleTriggerType triggerType; + private FailureStrategy failureStrategy; private WarningType warningType; @@ -87,6 +90,7 @@ public ScheduleVO(Schedule schedule) { this.setId(schedule.getId()); this.setCrontab(schedule.getCrontab()); this.setMissedFirePolicy(schedule.getMissedFirePolicy()); + this.setTriggerType(schedule.getTriggerType()); this.setProjectName(schedule.getProjectName()); this.setUserName(schedule.getUserName()); this.setWorkerGroup(schedule.getWorkerGroup()); diff --git a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/SchedulerServiceTest.java b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/SchedulerServiceTest.java index 3c9a9dedc696..7f077b8264c0 100644 --- a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/SchedulerServiceTest.java +++ b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/SchedulerServiceTest.java @@ -26,6 +26,7 @@ import org.apache.dolphinscheduler.common.enums.Priority; import org.apache.dolphinscheduler.common.enums.ReleaseState; import org.apache.dolphinscheduler.common.enums.ScheduleMissedFirePolicy; +import org.apache.dolphinscheduler.common.enums.ScheduleTriggerType; import org.apache.dolphinscheduler.common.enums.WarningType; import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.dolphinscheduler.dao.entity.Project; @@ -115,6 +116,17 @@ public void testScheduleParamMissedFirePolicyPresence() { Assertions.assertFalse(withoutPolicy.isMissedFirePolicySet()); Assertions.assertEquals(ScheduleMissedFirePolicy.SKIP_MISSED, withPolicy.getMissedFirePolicy()); Assertions.assertTrue(withPolicy.isMissedFirePolicySet()); + + ScheduleParam withoutTriggerType = JSONUtils.parseObject(scheduleWithoutPolicy, ScheduleParam.class); + ScheduleParam withTriggerType = JSONUtils.parseObject( + scheduleWithoutPolicy.replace("}", ",\"triggerType\":\"INTERVAL\"}"), ScheduleParam.class); + ScheduleParam withNullTriggerType = JSONUtils.parseObject( + scheduleWithoutPolicy.replace("}", ",\"triggerType\":null}"), ScheduleParam.class); + Assertions.assertFalse(withoutTriggerType.isTriggerTypeSet()); + Assertions.assertTrue(withTriggerType.isTriggerTypeSet()); + Assertions.assertEquals(ScheduleTriggerType.INTERVAL, withTriggerType.getTriggerType()); + Assertions.assertTrue(withNullTriggerType.isTriggerTypeSet()); + Assertions.assertNull(withNullTriggerType.getTriggerType()); } @ParameterizedTest @@ -158,6 +170,23 @@ user, projectCode, processDefinitionCode, scheduleExpression(null), WarningType. scheduleCaptor.getValue().getMissedFirePolicy()); } + @Test + public void testInsertScheduleDefaultsTriggerType() { + Mockito.when(projectDao.queryByCode(projectCode)).thenReturn(this.getProject()); + Mockito.when(scheduleDao.queryByWorkflowDefinitionCode(processDefinitionCode)).thenReturn(null); + Mockito.when(workflowDefinitionDao.queryByCode(processDefinitionCode)) + .thenReturn(Optional.of(this.getProcessDefinition())); + Mockito.when(scheduleDao.queryById(Mockito.anyInt())).thenReturn(new Schedule()); + + schedulerService.insertSchedule( + user, projectCode, processDefinitionCode, scheduleExpression(null), WarningType.NONE, 0, + FailureStrategy.CONTINUE, Priority.MEDIUM, "default", "tenantCode", environmentCode); + + ArgumentCaptor scheduleCaptor = ArgumentCaptor.forClass(Schedule.class); + Mockito.verify(scheduleDao).insert(scheduleCaptor.capture()); + Assertions.assertEquals(ScheduleTriggerType.CRON, scheduleCaptor.getValue().getTriggerType()); + } + @Test public void testInsertScheduleRejectsExplicitNullMissedFirePolicy() { assertInsertScheduleRejectsInvalidMissedFirePolicy("null"); @@ -204,6 +233,73 @@ user, projectCode, scheduleId, scheduleExpression(null), WarningType.NONE, 0, Assertions.assertEquals(ScheduleMissedFirePolicy.SKIP_MISSED, schedule.getMissedFirePolicy()); } + @Test + public void testUpdateSchedulePreservesTriggerTypeWhenOmitted() { + Schedule schedule = this.getSchedule(); + schedule.setReleaseState(ReleaseState.OFFLINE); + schedule.setTriggerType(ScheduleTriggerType.INTERVAL); + WorkflowDefinition workflowDefinition = this.getProcessDefinition(); + Mockito.when(projectDao.queryByCode(projectCode)).thenReturn(this.getProject()); + Mockito.when(scheduleDao.queryById(scheduleId)).thenReturn(schedule); + Mockito.when(workflowDefinitionDao.queryByCode(processDefinitionCode)) + .thenReturn(Optional.of(workflowDefinition)); + + schedulerService.updateSchedule( + user, projectCode, scheduleId, + "{\"startTime\":\"2019-12-16 00:00:00\",\"endTime\":\"2019-12-17 00:00:00\"," + + "\"crontab\":\"{\\\"second\\\":10,\\\"repeat\\\":-1}\"," + + "\"timezoneId\":\"Asia/Shanghai\"}", + WarningType.NONE, 0, FailureStrategy.CONTINUE, Priority.MEDIUM, "default", "tenantCode", + environmentCode); + + Assertions.assertEquals(ScheduleTriggerType.INTERVAL, schedule.getTriggerType()); + Assertions.assertEquals("{\"second\":10,\"repeat\":-1}", schedule.getCrontab()); + } + + @Test + public void testUpdateScheduleChangesTriggerTypeWhenProvided() { + Schedule schedule = this.getSchedule(); + schedule.setReleaseState(ReleaseState.OFFLINE); + schedule.setTriggerType(ScheduleTriggerType.INTERVAL); + WorkflowDefinition workflowDefinition = this.getProcessDefinition(); + Mockito.when(projectDao.queryByCode(projectCode)).thenReturn(this.getProject()); + Mockito.when(scheduleDao.queryById(scheduleId)).thenReturn(schedule); + Mockito.when(workflowDefinitionDao.queryByCode(processDefinitionCode)) + .thenReturn(Optional.of(workflowDefinition)); + + schedulerService.updateSchedule( + user, projectCode, scheduleId, + "{\"startTime\":\"2019-12-16 00:00:00\",\"endTime\":\"2019-12-17 00:00:00\"," + + "\"crontab\":\"0 0 6 * * ? *\",\"timezoneId\":\"Asia/Shanghai\"," + + "\"triggerType\":\"CRON\"}", + WarningType.NONE, 0, FailureStrategy.CONTINUE, Priority.MEDIUM, "default", "tenantCode", + environmentCode); + + Assertions.assertEquals(ScheduleTriggerType.CRON, schedule.getTriggerType()); + } + + @Test + public void testUpdateScheduleRejectsExplicitNullTriggerType() { + Schedule schedule = this.getSchedule(); + schedule.setReleaseState(ReleaseState.OFFLINE); + schedule.setTriggerType(ScheduleTriggerType.INTERVAL); + WorkflowDefinition workflowDefinition = this.getProcessDefinition(); + Mockito.when(projectDao.queryByCode(projectCode)).thenReturn(this.getProject()); + Mockito.when(scheduleDao.queryById(scheduleId)).thenReturn(schedule); + Mockito.when(workflowDefinitionDao.queryByCode(processDefinitionCode)) + .thenReturn(Optional.of(workflowDefinition)); + + Assertions.assertThrows(ServiceException.class, () -> schedulerService.updateSchedule( + user, projectCode, scheduleId, + "{\"startTime\":\"2019-12-16 00:00:00\",\"endTime\":\"2019-12-17 00:00:00\"," + + "\"crontab\":\"{\\\"second\\\":10,\\\"repeat\\\":-1}\"," + + "\"timezoneId\":\"Asia/Shanghai\",\"triggerType\":null}", + WarningType.NONE, 0, FailureStrategy.CONTINUE, Priority.MEDIUM, "default", "tenantCode", + environmentCode)); + Assertions.assertEquals(ScheduleTriggerType.INTERVAL, schedule.getTriggerType()); + Mockito.verify(scheduleDao, Mockito.never()).updateById(Mockito.any()); + } + @Test public void testUpdateScheduleRejectsExplicitNullMissedFirePolicy() { assertUpdateScheduleRejectsInvalidMissedFirePolicy("null"); diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/ScheduleTriggerType.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/ScheduleTriggerType.java new file mode 100644 index 000000000000..5700a00667ca --- /dev/null +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/ScheduleTriggerType.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.common.enums; + +import lombok.Getter; + +import com.baomidou.mybatisplus.annotation.EnumValue; + +@Getter +public enum ScheduleTriggerType { + + CRON(0, "Cron expression"), + INTERVAL(1, "Fixed interval"); + + @EnumValue + private final int code; + + private final String description; + + ScheduleTriggerType(int code, String description) { + this.code = code; + this.description = description; + } +} diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/IntervalSchedule.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/IntervalSchedule.java new file mode 100644 index 000000000000..3e7df610a298 --- /dev/null +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/IntervalSchedule.java @@ -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. + */ + +package org.apache.dolphinscheduler.common.utils; + +import lombok.Value; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Fixed interval schedule encoded as JSON. + */ +@Value +public class IntervalSchedule { + + long intervalMilliseconds; + + int repeatCount; + + public static IntervalSchedule parse(String expression) { + final ObjectNode values; + try { + values = JSONUtils.parseObject(expression); + } catch (Exception e) { + throw new IllegalArgumentException("Interval schedule expression must be a JSON object", e); + } + if (values == null || !values.isObject()) { + throw new IllegalArgumentException("Interval schedule expression must not be null"); + } + + int hours = valueOf(values, "hour", false); + int minutes = valueOf(values, "minute", false); + int seconds = valueOf(values, "second", false); + int repeat = valueOf(values, "repeat", true); + if (hours < 0 || minutes < 0 || seconds < 0 || repeat < -1) { + throw new IllegalArgumentException("Interval duration values must not be negative"); + } + if (minutes > 59 || seconds > 59) { + throw new IllegalArgumentException("Interval minutes and seconds must be between 0 and 59"); + } + + long intervalMilliseconds = Math.addExact( + Math.addExact(Math.multiplyExact(hours, 3_600_000L), Math.multiplyExact(minutes, 60_000L)), + Math.multiplyExact(seconds, 1_000L)); + if (intervalMilliseconds == 0) { + throw new IllegalArgumentException("Interval duration must be positive"); + } + return new IntervalSchedule(intervalMilliseconds, repeat); + } + + private static int valueOf(ObjectNode values, String key, boolean required) { + if (!values.has(key)) { + if (required) { + throw new IllegalArgumentException("Interval schedule field is required: " + key); + } + return 0; + } + if (!values.get(key).isInt()) { + throw new IllegalArgumentException("Interval schedule field must be an integer: " + key); + } + return values.get(key).intValue(); + } +} diff --git a/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/IntervalScheduleTest.java b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/IntervalScheduleTest.java new file mode 100644 index 000000000000..1d7e2b98b2c6 --- /dev/null +++ b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/IntervalScheduleTest.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.common.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class IntervalScheduleTest { + + @Test + void parseIntervalSchedule() { + IntervalSchedule intervalSchedule = + IntervalSchedule.parse("{\"hour\":1,\"minute\":2,\"second\":3,\"repeat\":4}"); + + assertEquals(3_723_000L, intervalSchedule.getIntervalMilliseconds()); + assertEquals(4, intervalSchedule.getRepeatCount()); + } + + @Test + void rejectInvalidIntervalSchedule() { + assertThrows(IllegalArgumentException.class, + () -> IntervalSchedule.parse("{\"second\":0}")); + assertThrows(IllegalArgumentException.class, + () -> IntervalSchedule.parse("{\"second\":-1,\"repeat\":-1}")); + assertThrows(IllegalArgumentException.class, + () -> IntervalSchedule.parse("{\"minute\":60,\"repeat\":1}")); + assertThrows(IllegalArgumentException.class, + () -> IntervalSchedule.parse("{\"second\":60,\"repeat\":1}")); + assertThrows(IllegalArgumentException.class, + () -> IntervalSchedule.parse("{\"second\":1}")); + } +} diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Schedule.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Schedule.java index 965a5c919477..9158357fb8f7 100644 --- a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Schedule.java +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Schedule.java @@ -21,6 +21,7 @@ import org.apache.dolphinscheduler.common.enums.Priority; import org.apache.dolphinscheduler.common.enums.ReleaseState; import org.apache.dolphinscheduler.common.enums.ScheduleMissedFirePolicy; +import org.apache.dolphinscheduler.common.enums.ScheduleTriggerType; import org.apache.dolphinscheduler.common.enums.WarningType; import java.util.Date; @@ -70,6 +71,8 @@ public class Schedule { private ScheduleMissedFirePolicy missedFirePolicy; + private ScheduleTriggerType triggerType; + private FailureStrategy failureStrategy; private WarningType warningType; diff --git a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ScheduleMapper.xml b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ScheduleMapper.xml index 78a401c9d1b2..e6620cd3be41 100644 --- a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ScheduleMapper.xml +++ b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ScheduleMapper.xml @@ -19,12 +19,12 @@ - id, workflow_definition_code, start_time, end_time, timezone_id, crontab, missed_fire_policy, failure_strategy, user_id, release_state, + id, workflow_definition_code, start_time, end_time, timezone_id, crontab, missed_fire_policy, trigger_type, failure_strategy, user_id, release_state, warning_type, warning_group_id, workflow_instance_priority, worker_group, tenant_code, environment_code, create_time, update_time ${alias}.id, ${alias}.workflow_definition_code, ${alias}.start_time, ${alias}.end_time, ${alias}.timezone_id, - ${alias}.crontab, ${alias}.missed_fire_policy, ${alias}.failure_strategy, ${alias}.user_id, ${alias}.release_state, ${alias}.warning_type, + ${alias}.crontab, ${alias}.missed_fire_policy, ${alias}.trigger_type, ${alias}.failure_strategy, ${alias}.user_id, ${alias}.release_state, ${alias}.warning_type, ${alias}.warning_group_id, ${alias}.workflow_instance_priority, ${alias}.worker_group, ${alias}.tenant_code, ${alias}.environment_code, ${alias}.create_time, ${alias}.update_time diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql index d69728be6ef3..a4dfa14ac00d 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql @@ -859,6 +859,7 @@ CREATE TABLE t_ds_schedules timezone_id varchar(40) DEFAULT NULL, crontab varchar(255) NOT NULL, missed_fire_policy tinyint NOT NULL DEFAULT 2, + trigger_type tinyint NOT NULL DEFAULT 0, failure_strategy tinyint(4) NOT NULL, user_id int(11) NOT NULL, release_state tinyint(4) NOT NULL, diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql index 361cdc3ff7fd..594835ce8b3f 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql @@ -860,6 +860,7 @@ CREATE TABLE `t_ds_schedules` ( `timezone_id` varchar(40) DEFAULT NULL COMMENT 'schedule timezone id', `crontab` varchar(255) NOT NULL COMMENT 'crontab description', `missed_fire_policy` tinyint NOT NULL DEFAULT '2' COMMENT 'missed fire policy: 0 skip missed, 1 fire once now, 2 fire all missed', + `trigger_type` tinyint NOT NULL DEFAULT '0' COMMENT 'schedule trigger type: 0 cron, 1 interval', `failure_strategy` tinyint(4) NOT NULL COMMENT 'failure strategy. 0:end,1:continue', `user_id` int(11) NOT NULL COMMENT 'user id', `release_state` tinyint(4) NOT NULL COMMENT 'release state. 0:offline,1:online ', diff --git a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql index 387716298493..09ad523b5fd5 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql @@ -786,6 +786,7 @@ CREATE TABLE t_ds_schedules ( timezone_id varchar(40) default NULL , crontab varchar(255) NOT NULL , missed_fire_policy smallint NOT NULL DEFAULT 2, + trigger_type smallint NOT NULL DEFAULT 0, failure_strategy int NOT NULL , user_id int NOT NULL , release_state int NOT NULL , diff --git a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.5.0_schema/mysql/dolphinscheduler_ddl.sql b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.5.0_schema/mysql/dolphinscheduler_ddl.sql index afeb9917b86b..500c1e27f9b1 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.5.0_schema/mysql/dolphinscheduler_ddl.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.5.0_schema/mysql/dolphinscheduler_ddl.sql @@ -20,3 +20,4 @@ ALTER TABLE `t_ds_workflow_instance` ADD INDEX idx_project_start_time (project_c ALTER TABLE `t_ds_schedules` ADD COLUMN `missed_fire_policy` tinyint NOT NULL DEFAULT '2' COMMENT 'missed fire policy: 0 skip missed, 1 fire once now, 2 fire all missed' AFTER `crontab`; +ALTER TABLE `t_ds_schedules` ADD COLUMN `trigger_type` tinyint NOT NULL DEFAULT '0' COMMENT 'schedule trigger type: 0 cron, 1 interval' AFTER `crontab`; diff --git a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.5.0_schema/postgresql/dolphinscheduler_ddl.sql b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.5.0_schema/postgresql/dolphinscheduler_ddl.sql index 1709fe57e737..4e0bb57112a9 100644 --- a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.5.0_schema/postgresql/dolphinscheduler_ddl.sql +++ b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.5.0_schema/postgresql/dolphinscheduler_ddl.sql @@ -19,3 +19,4 @@ CREATE INDEX idx_project_submit_time ON t_ds_task_instance (project_code ASC, su CREATE INDEX idx_project_start_time ON t_ds_workflow_instance (project_code ASC, start_time DESC); ALTER TABLE t_ds_schedules ADD COLUMN missed_fire_policy smallint NOT NULL DEFAULT 2; +ALTER TABLE t_ds_schedules ADD COLUMN trigger_type smallint NOT NULL DEFAULT 0; diff --git a/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzScheduler.java b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzScheduler.java index 70d5bf8fabc0..55e158d66b0e 100644 --- a/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzScheduler.java +++ b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzScheduler.java @@ -17,6 +17,7 @@ package org.apache.dolphinscheduler.scheduler.quartz; +import org.apache.dolphinscheduler.common.enums.ScheduleTriggerType; import org.apache.dolphinscheduler.dao.entity.Schedule; import org.apache.dolphinscheduler.scheduler.api.SchedulerApi; import org.apache.dolphinscheduler.scheduler.api.SchedulerException; @@ -24,10 +25,10 @@ import lombok.extern.slf4j.Slf4j; -import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.JobKey; import org.quartz.Scheduler; +import org.quartz.Trigger; import com.google.common.collect.Sets; @@ -52,16 +53,15 @@ public void start() throws SchedulerException { @Override public void insertOrUpdateScheduleTask(int projectId, Schedule schedule) throws SchedulerException { try { - CronTrigger cornTrigger = QuartzCornTriggerBuilder.newBuilder() - .withProjectId(projectId) - .withSchedule(schedule) - .build(); + Trigger trigger = schedule.getTriggerType() == ScheduleTriggerType.INTERVAL + ? QuartzSimpleTriggerBuilder.newBuilder().withProjectId(projectId).withSchedule(schedule).build() + : QuartzCornTriggerBuilder.newBuilder().withProjectId(projectId).withSchedule(schedule).build(); JobDetail jobDetail = QuartzJobDetailBuilder.newBuilder() .withProjectId(projectId) .withSchedule(schedule.getId()) .build(); - scheduler.scheduleJob(jobDetail, Sets.newHashSet(cornTrigger), true); - log.info("Success scheduleJob: {} with trigger: {} at quartz", jobDetail, cornTrigger); + scheduler.scheduleJob(jobDetail, Sets.newHashSet(trigger), true); + log.info("Success scheduleJob: {} with trigger: {} at quartz", jobDetail, trigger); } catch (Exception e) { log.error("Failed to add scheduler task, projectId: {}, scheduler: {}", projectId, schedule, e); throw new SchedulerException(QuartzSchedulerExceptionEnum.QUARTZ_UPSERT_JOB_ERROR, e); diff --git a/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSimpleTriggerBuilder.java b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSimpleTriggerBuilder.java new file mode 100644 index 000000000000..368757ddd84a --- /dev/null +++ b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/main/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSimpleTriggerBuilder.java @@ -0,0 +1,98 @@ +/* + * 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.scheduler.quartz; + +import org.apache.dolphinscheduler.common.enums.ScheduleMissedFirePolicy; +import org.apache.dolphinscheduler.common.utils.DateUtils; +import org.apache.dolphinscheduler.common.utils.IntervalSchedule; +import org.apache.dolphinscheduler.dao.entity.Schedule; + +import java.util.Date; + +import org.quartz.JobKey; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.SimpleTrigger; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; + +/** + * Builds a Quartz {@link SimpleTrigger} from a fixed-interval schedule expression. + */ +public class QuartzSimpleTriggerBuilder { + + private Integer projectId; + + private Schedule schedule; + + public static QuartzSimpleTriggerBuilder newBuilder() { + return new QuartzSimpleTriggerBuilder(); + } + + public QuartzSimpleTriggerBuilder withProjectId(Integer projectId) { + this.projectId = projectId; + return this; + } + + public QuartzSimpleTriggerBuilder withSchedule(Schedule schedule) { + this.schedule = schedule; + return this; + } + + public SimpleTrigger build() { + if (projectId == null) { + throw new IllegalArgumentException("projectId cannot be null"); + } + if (schedule == null) { + throw new IllegalArgumentException("schedule cannot be null"); + } + + IntervalSchedule intervalSchedule = IntervalSchedule.parse(schedule.getCrontab()); + JobKey jobKey = QuartzJobKey.of(projectId, schedule.getId()).toJobKey(); + TriggerKey triggerKey = TriggerKey.triggerKey(jobKey.getName(), jobKey.getGroup()); + Date startTime = DateUtils.transformTimezoneDate(schedule.getStartTime(), schedule.getTimezoneId()); + Date endTime = DateUtils.transformTimezoneDate(schedule.getEndTime(), schedule.getTimezoneId()); + Date now = new Date(); + if (startTime.before(now)) { + startTime = now; + } + + SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() + .withIntervalInMilliseconds(intervalSchedule.getIntervalMilliseconds()); + if (intervalSchedule.getRepeatCount() < 0) { + scheduleBuilder.repeatForever(); + } else { + scheduleBuilder.withRepeatCount(intervalSchedule.getRepeatCount()); + } + + ScheduleMissedFirePolicy missedFirePolicy = schedule.getMissedFirePolicy(); + if (missedFirePolicy == ScheduleMissedFirePolicy.SKIP_MISSED) { + scheduleBuilder.withMisfireHandlingInstructionNextWithExistingCount(); + } else if (missedFirePolicy == ScheduleMissedFirePolicy.FIRE_ONCE_NOW) { + scheduleBuilder.withMisfireHandlingInstructionFireNow(); + } else { + scheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires(); + } + + return TriggerBuilder.newTrigger() + .withIdentity(triggerKey) + .startAt(startTime) + .endAt(endTime) + .withSchedule(scheduleBuilder) + .build(); + } +} diff --git a/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/test/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSimpleTriggerBuilderTest.java b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/test/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSimpleTriggerBuilderTest.java new file mode 100644 index 000000000000..f917eb73f919 --- /dev/null +++ b/dolphinscheduler-scheduler-plugin/dolphinscheduler-scheduler-quartz/src/test/java/org/apache/dolphinscheduler/scheduler/quartz/QuartzSimpleTriggerBuilderTest.java @@ -0,0 +1,80 @@ +/* + * 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.scheduler.quartz; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.dolphinscheduler.common.enums.ScheduleMissedFirePolicy; +import org.apache.dolphinscheduler.dao.entity.Schedule; + +import java.util.Date; + +import org.junit.jupiter.api.Test; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; + +class QuartzSimpleTriggerBuilderTest { + + @Test + void buildIntervalTrigger() { + Schedule schedule = new Schedule(); + schedule.setId(2); + schedule.setCrontab("{\"minute\":5,\"repeat\":3}"); + schedule.setTimezoneId("UTC"); + schedule.setStartTime(new Date(System.currentTimeMillis() + 60_000)); + schedule.setEndTime(new Date(System.currentTimeMillis() + 3_600_000)); + + SimpleTrigger trigger = QuartzSimpleTriggerBuilder.newBuilder() + .withProjectId(1) + .withSchedule(schedule) + .build(); + + assertEquals(300_000L, trigger.getRepeatInterval()); + assertEquals(3, trigger.getRepeatCount()); + assertEquals(Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY, trigger.getMisfireInstruction()); + } + + @Test + void buildUnlimitedIntervalTriggerWithMisfirePolicies() { + for (ScheduleMissedFirePolicy policy : ScheduleMissedFirePolicy.values()) { + Schedule schedule = new Schedule(); + schedule.setId(2); + schedule.setCrontab("{\"second\":10,\"repeat\":-1}"); + schedule.setTimezoneId("UTC"); + schedule.setStartTime(new Date(System.currentTimeMillis() + 60_000)); + schedule.setEndTime(new Date(System.currentTimeMillis() + 3_600_000)); + schedule.setMissedFirePolicy(policy); + + SimpleTrigger trigger = QuartzSimpleTriggerBuilder.newBuilder() + .withProjectId(1) + .withSchedule(schedule) + .build(); + + assertEquals(SimpleTrigger.REPEAT_INDEFINITELY, trigger.getRepeatCount()); + int expectedInstruction; + if (policy == ScheduleMissedFirePolicy.SKIP_MISSED) { + expectedInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT; + } else if (policy == ScheduleMissedFirePolicy.FIRE_ONCE_NOW) { + expectedInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW; + } else { + expectedInstruction = Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY; + } + assertEquals(expectedInstruction, trigger.getMisfireInstruction()); + } + } +} diff --git a/dolphinscheduler-ui/src/locales/en_US/project.ts b/dolphinscheduler-ui/src/locales/en_US/project.ts index 534c11915b84..2e56d47e991e 100644 --- a/dolphinscheduler-ui/src/locales/en_US/project.ts +++ b/dolphinscheduler-ui/src/locales/en_US/project.ts @@ -149,6 +149,16 @@ export default { start_time: 'Start Time', end_time: 'End Time', crontab: 'Crontab', + trigger_type: 'Trigger Type', + cron_trigger: 'Cron Expression', + interval_trigger: 'Interval', + interval: 'Interval', + hours: 'Hours', + minutes: 'Minutes', + seconds: 'Seconds', + repeat: 'Execution Count', + unlimited_repeat_tip: '-1 means unlimited executions', + interval_must_be_positive: 'The interval must be greater than 0', missed_fire_policy: 'Missed Fire Policy', skip_missed: 'Skip missed executions', fire_once_now: 'Fire once immediately', diff --git a/dolphinscheduler-ui/src/locales/zh_CN/project.ts b/dolphinscheduler-ui/src/locales/zh_CN/project.ts index 429fd4990bf7..816d015ba12f 100644 --- a/dolphinscheduler-ui/src/locales/zh_CN/project.ts +++ b/dolphinscheduler-ui/src/locales/zh_CN/project.ts @@ -148,6 +148,16 @@ export default { start_time: '开始时间', end_time: '结束时间', crontab: 'Crontab', + trigger_type: '触发类型', + cron_trigger: 'Cron 表达式', + interval_trigger: '时间间隔', + interval: '时间间隔', + hours: '小时', + minutes: '分钟', + seconds: '秒', + repeat: '执行次数', + unlimited_repeat_tip: '-1 表示无限执行', + interval_must_be_positive: '时间间隔必须大于 0', missed_fire_policy: '定时错过策略', skip_missed: '跳过错过的执行,等待下一次调度', fire_once_now: '立即补触发一次,之后按正常节奏继续调度', diff --git a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/timing-modal.tsx b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/timing-modal.tsx index 5d15f06962a2..277884a212a7 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/timing-modal.tsx +++ b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/timing-modal.tsx @@ -36,6 +36,7 @@ import { NButton, NIcon, NInput, + NInputNumber, NSpace, NRadio, NRadioGroup, @@ -103,6 +104,11 @@ export default defineComponent({ ) const projectPreferences = ref({} as any) + const intervalHours = ref(1) + const intervalMinutes = ref(0) + const intervalSeconds = ref(0) + const intervalRepeat = ref(-1) + const cronExpression = ref(timingState.timingForm.crontab) const initProjectPreferences = (projectCode: number) => { queryProjectPreferenceByProjectCode(projectCode).then((result: any) => { @@ -185,6 +191,15 @@ export default defineComponent({ } const handlePreview = () => { + if ( + timingState.timingForm.triggerType === 'INTERVAL' && + intervalHours.value === 0 && + intervalMinutes.value === 0 && + intervalSeconds.value === 0 + ) { + window.$message.error(t('project.workflow.interval_must_be_positive')) + return + } getPreviewSchedule() } @@ -273,6 +288,57 @@ export default defineComponent({ const trim = getCurrentInstance()?.appContext.config.globalProperties.trim + const updateIntervalExpression = () => { + timingState.timingForm.crontab = JSON.stringify({ + hour: intervalHours.value, + minute: intervalMinutes.value, + second: intervalSeconds.value, + repeat: intervalRepeat.value + }) + } + + const restoreIntervalExpression = (expression: string): boolean => { + try { + const interval = JSON.parse(expression) + intervalHours.value = interval.hour || 0 + intervalMinutes.value = interval.minute || 0 + intervalSeconds.value = interval.second || 0 + intervalRepeat.value = interval.repeat ?? -1 + return true + } catch { + return false + } + } + + watch( + () => timingState.timingForm.triggerType, + (triggerType, previousTriggerType) => { + if (previousTriggerType === 'CRON') { + cronExpression.value = timingState.timingForm.crontab + } + + if (triggerType === 'CRON') { + timingState.timingForm.crontab = cronExpression.value + return + } + + if ( + previousTriggerType !== 'INTERVAL' && + !restoreIntervalExpression(timingState.timingForm.crontab) + ) { + updateIntervalExpression() + } + } + ) + + watch( + [intervalHours, intervalMinutes, intervalSeconds, intervalRepeat], + () => { + if (timingState.timingForm.triggerType === 'INTERVAL') { + updateIntervalExpression() + } + } + ) onMounted(() => { getWorkerGroups() getTenantList() @@ -293,10 +359,17 @@ export default defineComponent({ new Date(props.row.startTime), new Date(props.row.endTime) ] + const triggerType = props.row.triggerType || 'CRON' + timingState.timingForm.triggerType = triggerType timingState.timingForm.crontab = props.row.crontab - timingState.timingForm.timezoneId = props.row.timezoneId timingState.timingForm.missedFirePolicy = props.row.missedFirePolicy || 'FIRE_ALL_MISSED' + if (triggerType === 'CRON') { + cronExpression.value = props.row.crontab + } else if (!restoreIntervalExpression(props.row.crontab)) { + updateIntervalExpression() + } + timingState.timingForm.timezoneId = props.row.timezoneId timingState.timingForm.failureStrategy = props.row.failureStrategy timingState.timingForm.warningType = props.row.warningType timingState.timingForm.workflowInstancePriority = @@ -320,6 +393,10 @@ export default defineComponent({ renderLabel, updateWorkerGroup, handlePreview, + intervalHours, + intervalMinutes, + intervalSeconds, + intervalRepeat, ...toRefs(variables), ...toRefs(timingState), ...toRefs(props), @@ -354,33 +431,77 @@ export default defineComponent({ v-model:value={this.timingForm.startEndTime} /> - - - - {{ - trigger: () => ( - - ), - default: () => ( - - ) - }} - - - {t('project.workflow.execute_time')} - - + + + {this.timingForm.triggerType === 'CRON' ? ( + + + + {{ + trigger: () => ( + + ), + default: () => ( + + ) + }} + + + {t('project.workflow.execute_time')} + + + + ) : ( + +
+
+ + + + + + + + + +
+
+ + + + + {t('project.workflow.execute_time')} + +
+
+ {t('project.workflow.unlimited_repeat_tip')} +
+
+
+ )} { new Date(year + 100, month, day) ], crontab: '0 0 * * * ? *', + triggerType: 'CRON', + intervalHour: 0, + intervalMinute: 0, + intervalSecond: 0, + intervalRepeat: -1, timezoneId: Intl.DateTimeFormat().resolvedOptions().timeZone, missedFirePolicy: 'FIRE_ALL_MISSED', failureStrategy: 'CONTINUE', diff --git a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-modal.ts b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-modal.ts index 203ae5bc447e..81e70b3808c5 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-modal.ts +++ b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-modal.ts @@ -176,7 +176,9 @@ export function useModal( ) const data = { + triggerType: state.timingForm.triggerType, schedule: JSON.stringify({ + triggerType: state.timingForm.triggerType, startTime: start, endTime: end, crontab: state.timingForm.crontab, @@ -264,7 +266,9 @@ export function useModal( startTime: start, endTime: end, crontab: state.timingForm.crontab, - timezoneId: state.timingForm.timezoneId + timezoneId: state.timingForm.timezoneId, + missedFirePolicy: state.timingForm.missedFirePolicy, + triggerType: state.timingForm.triggerType }) previewSchedule({ schedule }, projectCode).then((res: any) => { variables.schedulePreviewList = res