APScheduler 源码阅读(四) triggers(一)

趁热打铁,学习一下 APSchedulerpython 的源码,很好奇任务调度控制的实现。

分析源码主要还是针对 APScheduler 下的几个关键的模块

  • events 事件
  • executors 执行器
  • job 任务
  • jobstores 任务存储
  • triggers 触发器
  • schedulers 调度程序

这一篇主要瞅瞅 triggers 触发器

分类

triggers 触发器主要分为 4

  • date : 在给定的日期时间触发一次。如果选择的是 data, 但是没有设置具体执行时间,则使用当前时间。
  • interval : 以固定时间间隔运行任务。如果设置了 start_time, 则从 start_time 开始触发,否则从 当前时间开始+间隔 开始触发
  • cron : 在匹配指定条件时触发, 与 UNIX cron 调度程序的工作原理类似。
  • combining : 以上触发器的混合使用模式
    • and : 始终返回所有给定触发器同时满足条件的最早的下次触发时间。
    • or : 始终返回由任何给定触发器产生的最早的下一次触发时间。

因为存在这几种触发器的选择,实现的时候基本也是按照这个思路封装的类,接下来从基类开始,逐个分析这些实现

BaseTrigger

在这里插入图片描述

基类中只定义了2个函数,其实也是 trigger 的核心

  • get_next_fire_time() : 根据触发器定义的规则,返回下一次执行的时间,如果没有则返回 None, 而规则如何转换成具体的时间,就需要交由子类自己实现,所以基类中定义的是 abstractmethod
  • _apply_jitter() : 给返回的下一次执行时间添加一个误差, 来提前或延迟 job 的执行

因为 get_next_fire_time() 是交由子类实现的,这里主要看一下 _apply_jitter() 的实现

def _apply_jitter(self, next_fire_time, jitter, now):
    if next_fire_time is None or not jitter:
        return next_fire_time

    next_fire_time_with_jitter = next_fire_time + timedelta(
            seconds=random.uniform(-jitter, jitter))

    if next_fire_time_with_jitter < now:
        return next_fire_time

    return next_fire_time_with_jitter
  • next_fire_time : get_next_fire_time() 获取到的下一次执行时间
  • jitter : 误差允许的时间值,单位秒
  • now : 当前时间

这个函数很简单,就是在 next_fire_time 基础上加上一个 [-jitter, jitter] 的误差,如果加完误差的新时间 next_fire_time_with_jitter 小于当前时间,则返回 next_fire_time, 不然返回 next_fire_time_with_jitter

BaseTrigger 部分只有这么多内容,接下来主要看具体的子类实现

DateTrigger

在传入的固定时间执行一次的模式.而且这个时间是初始化的时候传入的

初始化

def __init__(self, run_date=None, timezone=None):
    timezone = astimezone(timezone) or get_localzone()
    if run_date is not None:
        self.run_date = convert_to_datetime(run_date, timezone, 'run_date')
    else:
        self.run_date = datetime.now(timezone)

将传入时间根据时区调整一下时间,如果没有设置,则选择当前时间

get_next_fire_time()

所以获取下次执行时间,只需要返回 self.run_date 即可

def get_next_fire_time(self, previous_fire_time, now):
    return self.run_date if previous_fire_time is None else None

如何保证 data 模式下只执行一次, 这是一个挺有意思的方式, 但是需要结合一下 job 中这一段代码,它是先将 self.next_run_time 加到返回结果中,然后再调用 get_next_fire_time() 函数, 得到结果是 None, 返回了

def _get_run_times(self, now):
    run_times = []
    next_run_time = self.next_run_time
    while next_run_time and next_run_time <= now:
        run_times.append(next_run_time)
        next_run_time = self.trigger.get_next_fire_time(next_run_time, now)
        print(next_run_time)

    return run_times

schedulers 调度中为了确保任务只执行一次,也做了一些设计, 大致是下面这样的流程,想象一下 schedulers 在一直轮询查询下面这个过程, 实际代码逻辑比这个稍微再复杂一点

  • schedulers 先从 jobstores 中获取一些待执行的 job
  • 再从 job 中获取下次执行的时间信息, 其实也就是这次运行的时间
  • 在执行后,在通过 jobget_next_fire_time()查询下次执行时间
    • 如果查到的是 None, 直接删除任务,保证只运行一次
    • 反之将值更新到 job

序列化和反序列化

triggerjob 一样,都需要提供一个序列化和反序列化的接口,其实也不太算直接提供
因为如果看过上一章关于 jobstore 中对 redis 的介绍会知道,我们实际上是通过 cPickledumps() 序列化和 loads() 反序列对象
cPicklepython 内置的类型有自己逻辑,但是像 triggerjob 这些自定义的类在 序列化反序列化 的过程中需要调用它们的 __getstate__()__setstate__() 接口

def __getstate__(self):
    return {
        'version': 1,
        'run_date': self.run_date
    }

def __setstate__(self, state):
    # This is for compatibility with APScheduler 3.0.x
    if isinstance(state, tuple):
        state = state[1]

    if state.get('version', 1) > 1:
        raise ValueError(
            'Got serialized data for version %s of %s, but only version 1 can be handled' %
            (state['version'], self.__class__.__name__))

    self.run_date = state['run_date']

DateTrigger 比较简单,只有 version, run_date 信息需要序列化
但在我的认知里,version 其实可以不用保存下来,可能是为了在版本混用的情况下可以区分不同版本,但是我只看了 3.6.3 这一个版本,不确定它真正的作用

DateTrigger 算是很详细的分析完了,接下来换一个 子类 再来

IntervalTrigger

以固定时间间隔运行任务,我们还是从初始化开始看

初始化

def __init__(self, weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None,
             end_date=None, timezone=None, jitter=None):
    self.interval = timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes,
                              seconds=seconds)
    self.interval_length = timedelta_seconds(self.interval)
    if self.interval_length == 0:
        self.interval = timedelta(seconds=1)
        self.interval_lefuzhingth = 1

    if timezone:
        self.timezone = astimezone(timezone)
    elif isinstance(start_date, datetime) and start_date.tzinfo:
        self.timezone = start_date.tzinfo
    elif isinstance(end_date, datetime) and end_date.tzinfo:
        self.timezone = end_date.tzinfo
    else:
        self.timezone = get_localzone()

    start_date = start_date or (datetime.now(self.timezone) + self.interval)
    self.start_date = convert_to_datetime(start_date, self.timezone, 'start_date')
    self.end_date = convert_to_datetime(end_date, self.timezone, 'end_date')

    self.jitter = jitter

从传入参数就可以知道它提供的所有功能

  • weeks, days, hours, minutes, seconds : 间隔需要配置的时间,通过这些字段算出一个时间间隔
  • start_date : job 的开始时间,如果未设置,则是 datetime.now(self.timezone) + self.interval 当前时间加时间间隔作为开始时间
  • end_date : job 的结束时间
  • timezone : 时区
  • jitter : 提前或延迟 job 执行的误差时间

get_next_fire_time()

def get_next_fire_time(self, previous_fire_time, now):
    if previous_fire_time:
        next_fire_time = previous_fire_time + self.interval
    elif self.start_date > now:
        next_fire_time = self.start_date
    else:
        timediff_seconds = timedelta_seconds(now - self.start_date)
        next_interval_num = int(ceil(timediff_seconds / self.interval_length))
        next_fire_time = self.start_date + self.interval * next_interval_num

    if self.jitter is not None:
        next_fire_time = self._apply_jitter(next_fire_time, self.jitter, now)

    if not self.end_date or next_fire_time <= self.end_date:
        return self.timezone.normalize(next_fire_time)

因为是以固定时间间隔运行任务,所以计算下次执行时间的核心部分就是 next_fire_time = previous_fire_time + self.interval
剩下就是需要保证这个时间在 [开始时间,结束时间] 范围内,以及需要判断是否调用 _apply_jitter()

计算 next_fire_time 值部分 if 中前 2 种判断很好理解,我这里主要解释一下 previous_fire_timeNoneself.start_date < now 是在哪种情况下触发

大部分时候我们在使用 IntervalTrigger 时, 只会设置开始时间,结束时间以及时间间隔,不会设置 next_run_time, 此时 next_run_time 为空
schedulers 在添加任务时候会先判断 jobnext_run_time 是否为空,为空时手动调用一下 get_next_fire_time(None, now) 函数,获取任务的下次执行时间,并重新赋值给 job, 此时也就出现了一种新的逻辑

举个更详细的例子, 我开始时间设置的 13 点, 每隔 1 小时执行一次,所以我预期的结果是 14 点, 15 点, 16 点等等整点执行, 但是当我启动程序的时候是 14 点 30 分, 也就是满足了上面previous_fire_timeNoneself.start_date < now 的条件, 此时我要计算下次执行的时间, 就是计算 开始时间现在时间 中间相当于有几个 (next_interval_num) 时间间隔, 时间间隔通过 ceil() 向上取整, self.start_date + self.interval * next_interval_num 就可以得出新的 next_fire_time

最后的 return 又省略了一部分代码,下次执行时间大于 self.end_date, 直接返回 None

return self.timezone.normalize(next_fire_time) if (not self.end_date or next_fire_time <= self.end_date) else None

序列化和反序列化

DateTrigger 差不多,保存核心信息 timezone start_date, end_date, interval, jitter

def __getstate__(self):
    return {
        'version': 2,
        'timezone': self.timezone,
        'start_date': self.start_date,
        'end_date': self.end_date,
        'interval': self.interval,
        'jitter': self.jitter,
    }

反序列化的代码也比较简单,这里就不粘贴

小结

比较详细的介绍了基类 BaseTrigger 以及在固定时间执行一次的 DateTrigger 和以固定时间间隔运行任务的 IntervalTrigger,整体代码难度不大~

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会偷懒的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值