更新
距离上篇 调度系统中不同周期任务依赖的方法(1) 写完发布已经将近一个月了,回顾上篇,我们介绍了任务周期,任务依赖的方法。写完上篇,作者以为剩下的工作也就是在前面的基础上修修补补,关于依赖的问题,已经找到一个简单完美的方案。不过很快,我们就发现了一些问题。
假设
上篇我们所有的讨论其实都有一个前提,就是认为每个作业的 cron 表达式有一个固定的触发频率,比如每天、每小时、每 5 分钟执行一次,周期都是固定,我们所有周期偏移计算都是基于这个固定周期来推断的。假如这个假设是错的,就意味我们讨论的所有问题都存在缺陷,是不可靠的。
很悲剧的是 cron 表达式还有这种形式,01 */5 13-14 * * ?
,即每天 13 ~14 点之间每 5 分钟的 01 秒触发:
2020-01-03 14:50:01
2020-01-03 14:55:01
2020-01-04 13:05:01
其中,1, 2 之间的间隔是 5 分钟,2,3 之前的间隔将近一天,也就是说周期是变化的。
修正
其实,实际中恐怕很少会有这样的数据计算任务,从我们上一篇的讲述中可知,计划调度时间对应一个数据时间,即每次运行任务隐含了一个数据范围,比如天级的任务是对昨天一整天产生的数据计算,小时任务是对过去一小时产生的数据计算,而上面这种运行周期不定的调度计划,数据计算范围一会是 5 分钟,一会是 1 天,不符合常规的做法。不过,既然我们希望提供一个让用户满意的依赖方案,就需要尽可能地考虑这种种特殊的情况,因此作者做了一点小小努力,也就把前篇提到的方法基本推翻,重做了一遍。
首先需要解决的是,如果周期不固定,我们如何通过偏移找到前后调度时间。Quartz 的 API 里提供计算 next fire time 的方法,也提供了获取 previous fire time 的方法,但是这个方法一直空着,而且我看一下 github 的提交记录,这一空就是好多年。
好在看到网上有位同仁给出了一个递归求解上一个调度时间的方法,不甚感激,使用中有些小问题,小调整后也可以解决。
quartz 计算上次触发时间
https://my.oschina.net/u/139350/blog/275293
因此我们最终会有这样一个工具类,有意区别于前篇的 CronUtils
public class QuartzUtils {
public static LocalDateTime scheduleTime(String cronExpression, LocalDateTime someTime) {
...
}
public static LocalDateTime nextScheduleTime(String cronExpression, LocalDateTime scheduleTime) {
...
}
public static LocalDateTime preScheduleTime(String cronExpression, LocalDateTime scheduleTime) {
...
}
}
有了这些方法之后,我们可以试着提供一个简单通用的方法,对于不管是同周期还是不同周期任务的相互依赖,只需要提供子任务的调度时间,cron 表达式以及父任务的 cron 表达式,就可以计算出子任务依赖父任务的 schedule time,进而可以找到有没有对应的任务执行成功,来判断依赖是否满足。更进一步地,如果父任务是否是自依赖这个条件也一并可以塞进去,然后输出最终结果,一个或多个父任务的调度时刻。
如果要写成一个方法,大致是这样的,
public List<LocalDateTime> parentScheduleTimes(String childCron, LocalDateTime childScheduleTime, String parentCron, boolean parentDependOnPast)
由于方法参数比较多,我们可以参考 bulider 模式做成一个 DependencyBuilder
public class DependencyBuilder {
private final String parentCronExpression;
private final String childCronExpression;
private final LocalDateTime childScheduleTime;
private final boolean isParentSelfDepend;
private DependencyBuilder(Builder builder) {
this.parentCronExpression = builder.getParentCronExpression();
this.childCronExpression = builder.getChildCronExpression();
this.childScheduleTime = builder.getChildScheduleTime();
this.isParentSelfDepend = builder.isParentSelfDepend();
}
public static Builder builder() {
return new Builder();
}
@Getter
public static class Builder {
private String parentCronExpression;
private String childCronExpression;
private LocalDateTime childScheduleTime;
private boolean isParentSelfDepend = false;
public Builder parentCronExpression(String parentCronExpression) {
this.parentCronExpression = parentCronExpression;
return this;
}
public Builder childCronExpression(String childCronExpression) {
this.childCronExpression = childCronExpression;
return this;
}
public Builder childScheduleTime(LocalDateTime childScheduleTime) {
this.childScheduleTime = childScheduleTime;
return this;
}
public Builder isParentSelfDepend(boolean isParentSelfDepend) {
this.isParentSelfDepend = isParentSelfDepend;
return this;
}
public DependencyBuilder build() {
return new DependencyBuilder(this);
}
}
public List<LocalDateTime> parentScheduleTimes() {
requireNonNull(parentCronExpression, "Parent cron expression is null.");
requireNonNull(childCronExpression, "Child cron expression is null.");
requireNonNull(childScheduleTime, "Child schedule time is null.");
checkArgument(isSatisfiedBy(childCronExpression, childScheduleTime),
"Cron " + childCronExpression + " is not satisfied by " + childScheduleTime);
try {
List<LocalDateTime> scheduleTimes = compute().stream()
.map(d -> localDateTime(d))
.collect(Collectors.toList());
if (scheduleTimes.isEmpty()) {
return scheduleTimes;
}
if (isParentSelfDepend) {
return asList(scheduleTimes.get(scheduleTimes.size() - 1));
}
return scheduleTimes;
} catch (Exception e) {
log.warn("Compute {} schedule times fail.", this, e);
return Lists.emptyList();
}
}
private List<Date> compute() throws Exception {
//主要不同周期的依赖的计算逻辑
}
@Override
public String toString() {
return "DependencyBuilder{" +
"parentCronExpression='" + parentCronExpression + '\'' +
", childCronExpression='" + childCronExpression + '\'' +
", childScheduleTime=" + childScheduleTime +
", isParentSelfDepend=" + isParentSelfDepend +
'}';
}
}
验证
我们可以像上篇一样,在单元测试中对各依赖情况做测试验证
public class DependencyBuilderTest {
@Test
public void when_recursive_is_deep() throws Exception {
String parentCron = "01 */5 13-23 * * ?";
String childCron = "00 */5 13-23 * * ?";
DependencyBuilder checker = builder().parentCronExpression(parentCron)
.childCronExpression(childCron)
.childScheduleTime(parse("2019-12-11 13:00:00"))
.build();
assertThat(checker.parentScheduleTimes()).isEqualTo(asList(parse("2019-12-11 13:00:01")));
}
@Test
public void when_recursive_is_deep_2() throws Exception {
String parentCron = "00 */5 13-23 * * ?";
String childCron = "02 */5 13-23 * * ?";
DependencyBuilder checker = builder().parentCronExpression(parentCron)
.childCronExpression(childCron)
.childScheduleTime(parse("2019-12-11 13:00:02"))
.build();
assertThat(checker.parentScheduleTimes()).isEqualTo(asList(parse("2019-12-11 13:00:00")));
}
@Test
public void when_recursive_is_deep_3() throws Exception {
String parentCron = "00 */5 13-23 * * ?";
String childCron = "00 */5 13-23 * * ?";
DependencyBuilder checker = builder().parentCronExpression(parentCron)
.childCronExpression(childCron)
.childScheduleTime(parse("2019-12-11 13:00:00"))
.build();
assertThat(checker.parentScheduleTimes()).isEqualTo(asList(parse("2019-12-11 13:00:00")));
}
...
}
题外话
任何学科都有一个假设,比如经济学的假设是人都是理性的,任何时候都会基于理性做出利益最大化的选择,这个假设既是前提也是局限所在,科学重要的不仅是证实,而且还要能证伪。周期依赖这个还谈不上科学的小问题,其实也有它的假设,比如根据调度时间推断数据时间,比如数据时间都是根据自然天、小时等自然周期,等等,也因为这些假设,此文描述的方法有它适用的范围。