调度系统中不同周期任务依赖的方法(2)

更新

距离上篇 调度系统中不同周期任务依赖的方法(1) 写完发布已经将近一个月了,回顾上篇,我们介绍了任务周期,任务依赖的方法。写完上篇,作者以为剩下的工作也就是在前面的基础上修修补补,关于依赖的问题,已经找到一个简单完美的方案。不过很快,我们就发现了一些问题。

假设

上篇我们所有的讨论其实都有一个前提,就是认为每个作业的 cron 表达式有一个固定的触发频率,比如每天、每小时、每 5 分钟执行一次,周期都是固定,我们所有周期偏移计算都是基于这个固定周期来推断的。假如这个假设是错的,就意味我们讨论的所有问题都存在缺陷,是不可靠的。

很悲剧的是 cron 表达式还有这种形式,01 */5 13-14 * * ?,即每天 13 ~14 点之间每 5 分钟的 01 秒触发:

  1. 2020-01-03 14:50:01
  2. 2020-01-03 14:55:01
  3. 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")));
    }
...
}

题外话

任何学科都有一个假设,比如经济学的假设是人都是理性的,任何时候都会基于理性做出利益最大化的选择,这个假设既是前提也是局限所在,科学重要的不仅是证实,而且还要能证伪。周期依赖这个还谈不上科学的小问题,其实也有它的假设,比如根据调度时间推断数据时间,比如数据时间都是根据自然天、小时等自然周期,等等,也因为这些假设,此文描述的方法有它适用的范围。

调度系统

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值