Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9a4d563
feat(schedule): add quartz trigger types
liang-wenjie Jul 29, 2026
0245abd
merge: add quartz trigger types
liang-wenjie Jul 29, 2026
89cd1ed
feat(schedule): add missed fire policy
liang-wenjie Aug 4, 2026
c73327e
refactor(schedule): apply review suggestions
liang-wenjie Aug 5, 2026
2868f09
fix(schedule): preserve existing misfire behavior
liang-wenjie Aug 5, 2026
060743f
fix: address missed fire policy review feedback
liang-wenjie Aug 6, 2026
dcf1d0d
fix(schedule): serialize missedFirePolicy and preserve existing polic…
liang-wenjie Aug 7, 2026
f253dc4
Merge branch 'dev' into 2dev/feat/add-schedule-missed-fire-policy-v2
SbloodyS Aug 7, 2026
c804e50
test(api): fix schedule insert mock
liang-wenjie Aug 7, 2026
cd5a311
Merge remote-tracking branch 'origin/2dev/feat/add-schedule-missed-fi…
liang-wenjie Aug 7, 2026
1831d55
fix(api): reject invalid missed fire policy
liang-wenjie Aug 10, 2026
b362b9d
Revert "merge: add quartz trigger types"
liang-wenjie Aug 12, 2026
907e212
merge: add missed fire policy
liang-wenjie Aug 12, 2026
8bc4d1a
merge: sync latest dev
liang-wenjie Aug 12, 2026
03b330b
feat(schedule): add interval trigger type
liang-wenjie Aug 12, 2026
3c84090
fix(schedule): remove invalid source characters
liang-wenjie Aug 12, 2026
c30e220
fix(ui): add interval schedule translations
liang-wenjie Aug 12, 2026
e7a5948
style(ui): improve interval schedule layout
liang-wenjie Aug 12, 2026
1094eab
fix(ui): restore chinese schedule translations
liang-wenjie Aug 12, 2026
f492927
style(ui): match repeat input width
liang-wenjie Aug 12, 2026
5b1c6a2
fix(ui): correct interval layout jsx
liang-wenjie Aug 12, 2026
ef3655e
fix(schedule): persist interval trigger type
liang-wenjie Aug 17, 2026
972aa48
test(schedule): cover interval trigger API flow
liang-wenjie Aug 17, 2026
f48a528
fix: harden interval schedule trigger handling
liang-wenjie Aug 18, 2026
6dda72b
fix(ui): add spacing for unlimited repeat tip
liang-wenjie Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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<String, Object>) ((List<LinkedHashMap>) queryScheduleListResponse.getBody()
.getData()).get(0)).get("id");
}
Expand Down Expand Up @@ -156,14 +158,16 @@ 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());

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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() {
}

Expand All @@ -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{"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -389,6 +395,20 @@ public List<String> 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<ZonedDateTime> 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());
Expand All @@ -403,6 +423,24 @@ public List<String> 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
*
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,6 +58,8 @@ public class ScheduleVO {

private ScheduleMissedFirePolicy missedFirePolicy;

private ScheduleTriggerType triggerType;

private FailureStrategy failureStrategy;

private WarningType warningType;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Schedule> scheduleCaptor = ArgumentCaptor.forClass(Schedule.class);
Mockito.verify(scheduleDao).insert(scheduleCaptor.capture());
Assertions.assertEquals(ScheduleTriggerType.CRON, scheduleCaptor.getValue().getTriggerType());
}

@Test
public void testInsertScheduleRejectsExplicitNullMissedFirePolicy() {
assertInsertScheduleRejectsInvalidMissedFirePolicy("null");
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading