简介

APScheduler是一个小巧而强大的Python类库,通过它你可以实现类似Unix系统cronjob类似的定时任务系统。使用之余,阅读一下源码,一方面有助于更好的使用它,另一方面,个人认为aps的架构设计质量很高,阅读它对于提升软件开发的sense很有帮助。

组成

APScheduler整个系统可以说由这五个概念组成:

  • scheduler:控制器,可以看做整个系统的driver,外部世界通过它来实现任务(Job)的增删改查管理。根据IO模式的不同,aps提供了多种scheduler实现。
  • job:描述一个任务本身。
  • jobstore:任务持久化仓库。aps提供了内存、redis、mongodb、sqlalchemy几种store
  • executor:执行任务的模块。根据不同的IO模型有多种executor选择。
  • trigger:描述一个任务何时被触发,有按日期、按时间间隔、按cronjob描述式三种触发方式

这样的划分充分发挥了软件设计中抽象的威力,我们下面对每个模块进行描述

scheduler

BaseScheduler类是所有scheduler的抽象基类,它的初始化代码是这样的:

     def __init__(self, gconfig={}, **options):
super(BaseScheduler, self).__init__()
self._executors = {}
self._executors_lock = self._create_lock()
self._jobstores = {}
self._jobstores_lock = self._create_lock()
self._listeners = []
self._listeners_lock = self._create_lock()
self._pending_jobs = []
self.configure(gconfig, **options)

可以看到一个scheduler维护了自己的executor和jobstore表,通过configure方法进行初始化。在configure中,scheduler读取传入的配置,对executors和jobstores进行初始化,一个典型的配置是这样的:

 APS_SCHEDULER_CONFIG = {
'jobstores': {
'default': {'type': 'sqlalchemy', 'url': 'postgres://127.0.0.1:5432/optimus'},
},
'executors': {
'default': {'type': 'processpool', 'max_workers': 10}
},
'job_defaults': {
'coalesce': True,
'max_instances': 5,
'misfire_grace_time': 30
},
'timezone': 'Asia/Shanghai'
}

如果我们把APS_SCHEDULER_CONFIG作为options传入给一个scheduler,会产生什么结果呢?首先,我们添加了一个默认(名叫default)的jobstore,它的具体实现类型是sqlalchemy,数据库连接url是指向一个本地postgresql数据库,也就是说添加到这个scheduler的job会默认使用这个jobstore进行存储。其次,我们添加了一个默认的executor,他是一个多进程实现,也就是说每个job在运行时,是通过一个进程池来作为worker实际执行的,这个进程池最大size是10。job_defaults参数定义了一些特殊行为:

  • coalesce:当由于某种原因导致某个job积攒了好几次没有实际运行(比如说系统挂了5分钟后恢复,有一个任务是每分钟跑一次的,按道理说这5分钟内本来是“计划”运行5次的,但实际没有执行),如果coalesce为True,下次这个job被submit给executor时,只会执行1次,也就是最后这次,如果为False,那么会执行5次(不一定,因为还有其他条件,看后面misfire_grace_time的解释)
  • max_instance: 就是说同一个job同一时间最多有几个实例再跑,比如一个耗时10分钟的job,被指定每分钟运行1次,如果我们max_instance值为5,那么在第6~10分钟上,新的运行实例不会被执行,因为已经有5个实例在跑了
  • misfire_grace_time:设想和上述coalesce类似的场景,如果一个job本来14:00有一次执行,但是由于某种原因没有被调度上,现在14:01了,这个14:00的运行实例被提交时,会检查它预订运行的时间和当下时间的差值(这里是1分钟),大于我们设置的30秒限制,那么这个运行实例不会被执行。

这里还需要指出的一点是,为什么scheduler的配置可以写成这种json形式,而scheduler会正确地找到对应的实现类进行初始化?这里运用了两个技巧:

entry point

用python egg的机制把各个组件注册了成了entry point,如下所示

 [apscheduler.executors]
asyncio = apscheduler.executors.asyncio:AsyncIOExecutor
debug = apscheduler.executors.debug:DebugExecutor
gevent = apscheduler.executors.gevent:GeventExecutor
processpool = apscheduler.executors.pool:ProcessPoolExecutor
threadpool = apscheduler.executors.pool:ThreadPoolExecutor
twisted = apscheduler.executors.twisted:TwistedExecutor [apscheduler.jobstores]
memory = apscheduler.jobstores.memory:MemoryJobStore
mongodb = apscheduler.jobstores.mongodb:MongoDBJobStore
redis = apscheduler.jobstores.redis:RedisJobStore
sqlalchemy = apscheduler.jobstores.sqlalchemy:SQLAlchemyJobStore [apscheduler.triggers]
cron = apscheduler.triggers.cron:CronTrigger
date = apscheduler.triggers.date:DateTrigger
interval = apscheduler.triggers.interval:IntervalTrigger

这样,在scheduler模块中就可以用entry point的名称反查出对应组件

     _trigger_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.triggers'))
_trigger_classes = {}
_executor_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.executors'))
_executor_classes = {}
_jobstore_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.jobstores'))
_jobstore_classes = {}
_stopped = True

从而实现了一个便利的插件机制

ref_to_obj

另外通过一个加载函数完成"apscheduler.executors.pool:ThreadPoolExecutor"字符串到ThreadPoolExecutor类对象的查询

 def ref_to_obj(ref):
"""
Returns the object pointed to by ``ref``. :type ref: str
""" if not isinstance(ref, six.string_types):
raise TypeError('References must be strings')
if ':' not in ref:
raise ValueError('Invalid reference') modulename, rest = ref.split(':', 1)
try:
obj = __import__(modulename)
except ImportError:
raise LookupError('Error resolving reference %s: could not import module' % ref) try:
for name in modulename.split('.')[1:] + rest.split('.'):
obj = getattr(obj, name)
return obj
except Exception:
raise LookupError('Error resolving reference %s: error looking up object' % ref)

scheduler的主循环(main_loop),其实就是反复检查是不是有到时需要执行的任务,完成一次检查的函数是_process_jobs, 这个函数做这么几件事:

  1. 询问自己的每一个jobstore,有没有到期需要执行的任务(jobstore.get_due_jobs())
  2. 如果有,计算这些job中每个job需要运行的时间点(run_times = job._get_run_times(now))如果run_times有多个,这种情况我们上面讨论过,有coalesce检查
  3. 提交给executor排期运行(executor.submit_job(job, run_times))

那么在这个_process_jobs的逻辑,什么时候调用合适呢?如果不间断地调用,而实际上没有要执行的job,是一种浪费。每次掉用_process_jobs后,其实可以预先判断一下,下一次要执行的job(离现在最近的)还要多长时间,作为返回值告诉main_loop, 这时主循环就可以去睡一觉,等大约这么长时间后再唤醒,执行下一次_process_jobs。这里唤醒的机制就会有IO模型的区别了

scheduler由于IO模型的不同,可以有多种实现,如

  • BlockingScheduler:main_loop就在当前进程的主线程内运行,所以调用start函数后会阻塞当前线程。通过一个threading.Event条件变量对象完成scheduler的定时唤醒。
  • BackgroundScheduler:和BlockingScheduler基本一样,除了main_loop放在了单独线程里,所以调用start后主线程不会阻塞
  • AsyncIOScheduler:使用asyncio作为IO模型的scheduler,和AsyncIOExecutor配合使用,用asynio中event_loop的call_later完成定时唤醒
  • GeventScheduler:和BlockingScheduler基本一样,使用gevent作为IO模型,和GeventExecutor配合使用
  • QtScheduler:使用QTimer完成定时唤醒
  • TornadoScheduler:使用tornado的IO模型,用ioloop.add_timeout完成定时唤醒
  • TwistedScheduler:配合TwistedExecutor,用reactor.callLater完成定时唤醒

JobStore

jobstore提供给scheduler一个序列化jobs的统一抽象,提供对scheduler中job的增删改查接口,根据存储backend的不同,分以下几种

  • MemoryJobStore:没有序列化,jobs就存在内存里,增删改查也都是在内存中操作
  • SQLAlchemyJobStore:所有sqlalchemy支持的数据库都可以做为backend,增删改查操作转化为对应backend的sql语句
  • MongoDBJobStore:用mongodb作backend
  • RedisJobStore: 用redis作backend

除了MemoryJobStore外,其他几种都使用pickle做序列化工具,所以这里要指出一点,如果你不是在用内存做jobstore,那么必须确保你提供给job的可执行函数必须是可以被全局访问的,也就是可以通过ref_to_obj反查出来的,否则无法序列化。

使用数据库做jobstore,就会发现,其实创建了一张有三个域的的jobs表,分别是id, next_run_time, job_state,其中job_state是job对象pickle序列化后的二进制,而id和next_run_time则是支持job的两类查询(按id和按最近运行时间)

Executor

aps把任务最终的执行机制也抽象了出来,可以根据IO模型选配,不需要讲太多,最常用的是threadpool和processpoll两种(来自concurrent.futures的线程/进程池)。

不同类型的executor实现自己的_do_submit_job,完成一次实际的任务实例执行。以线程/进程池实现为例

     def _do_submit_job(self, job, run_times):
def callback(f):
exc, tb = (f.exception_info() if hasattr(f, 'exception_info') else
(f.exception(), getattr(f.exception(), '__traceback__', None)))
if exc:
self._run_job_error(job.id, exc, tb)
else:
self._run_job_success(job.id, f.result()) f = self._pool.submit(run_job, job, job._jobstore_alias, run_times, self._logger.name)
f.add_done_callback(callback)

Trigger

trigger是抽象出了“一个job是何时被触发”这个策略,每种trigger实现自己的get_next_fire_time函数

     @abstractmethod
def get_next_fire_time(self, previous_fire_time, now):
"""
Returns the next datetime to fire on, If no such datetime can be calculated, returns ``None``. :param datetime.datetime previous_fire_time: the previous time the trigger was fired
:param datetime.datetime now: current datetime
"""

aps提供的trigger包括:

  • date:一次性指定日期
  • interval:在某个时间范围内间隔多长时间执行一次
  • cron:和unix crontab格式兼容,最为强大

总结

简要介绍了apscheduler类库的组成,强调抽象概念的理解

APScheduler 3.0.1浅析的更多相关文章

  1. Python定时任务框架APScheduler 3.0.3 Cron示例

    APScheduler是基于Quartz的一个Python定时任务框架,实现了Quartz的所有功能,使用起来十分方便.提供了基于日期.固定时间间隔以及crontab类型的任务,并且可以持久化任务.基 ...

  2. APScheduler API -- apscheduler.triggers.cron

    apscheduler.triggers.cron API Trigger alias for add_job(): cron class apscheduler.triggers.cron.Cron ...

  3. python apscheduler的使用

    from apscheduler.schedulers.blocking import BlockingSchedulerfrom datetime import datetime def my_jo ...

  4. pip安装第三方库以及版本

    这篇blog只是写给自己看看的. 今天突然遇到sqlalchemy映射到数据库时,一个字段类型是datetime(6),我这边死活访问不上,之前一直没有问题,最后查明原因,原来是第三方库的版本问题,真 ...

  5. python apsheduler cron 参数解析

    from:https://apscheduler.readthedocs.io/en/v2.1.2/cronschedule.html Cron-style scheduling This is th ...

  6. 什么是PHP7中的孤儿进程与僵尸进程

    什么是PHP7中的孤儿进程与僵尸进程 基本概念 我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程.子进程的结束和父进程的运行是一个异步过程,即父进程永远无法 ...

  7. SpringSecurity原理

    一.认证的两种方式的介绍 1. 基于Session的认证方式 在之前的单体架构时代,我们认证成功之后都会将信息存入到Session中,然后响应给客户端的是对应的Session中数据的key,客户端会将 ...

  8. ZAM 3D 制作简单的3D字幕 流程(二)

    原地址:http://www.cnblogs.com/yk250/p/5663907.html 文中表述仅为本人理解,若有偏差和错误请指正! 接着 ZAM 3D 制作简单的3D字幕 流程(一) .本篇 ...

  9. ZAM 3D 制作3D动画字幕 用于Xaml导出

    原地址-> http://www.cnblogs.com/yk250/p/5662788.html 介绍:对经常使用Blend做动画的人来说,ZAM 3D 也很好上手,专业制作3D素材的XAML ...

随机推荐

  1. 好好讲一讲,到底什么是Java高级架构师!

    一. 什么是架构师 曾经有这么个段子: 甲:我已经应聘到一家中型软件公司了,今天上班的时候,全公司的人都来欢迎我. 乙:羡慕ing,都什么人来了? 甲:CEO.COO.CTO.All of 程序员,还 ...

  2. MySQL数据库常见问题1:关于 “ MySQL Installer is running in Community mode ” 的解决办法

     现象: MYSQL在安装完成后,系统能正常运行,但是第二天出现了如下一个提示框,如下图:  给个人人都看得懂的如下图: 解决办法:      这个是新版本MySQL服务自带的一个定时任务,每天23: ...

  3. Vue介绍:vue项目搭建

    一.环境搭建 二.项目创建 三.认识项目 四.项目功能 一.环境搭建 *安装node 官网下载安装包,傻瓜式安装:https://nodejs.org/zh-cn/ *安装cnpm npm insta ...

  4. cifs

    加入cifs 能把设备的目录挂载到我们的电脑上面 mount -t cifs -o username=Everyone //192.168.23.72/cifs /cif 如果当前是everyone ...

  5. HandlerMethodArgumentResolver完美解决 springmvc注入参数多传报错

    作为一个后端开发,能友好兼容前端参数传入错误等问题,在前端发布不小心多传一个参数导致系统错误的问题,一个广告系统是零容忍的,所以为了不犯错误,后端接收参数必须摒弃spring 的自动注入@Reques ...

  6. jquery属性方法hasClass判断是否存在某个class

    判断匹配集合中是否存在至少一个元素使用样式'selected',存在则返回'true',不存在为'flase'. <html> <head> <script src=&q ...

  7. jQuery.trim()方法

    定义和用法 $.trim() 函数用于去除字符串两端的空白字符. 注意:$.trim()函数会移除字符串开始和末尾处的所有换行符,空格(包括连续的空格)和制表符.如果这些空白字符在字符串中间时,它们将 ...

  8. oracle ORA-16038 ORA-00257

    出现 ORA-16038时也可以参考下边的错误,好像有关联影响 出现ORA-00257错误(空间不足错误),通过查找资料,绝大部分说这是由于归档日志太多,占用了全部的硬盘剩余空间导致的,通过简单删除日 ...

  9. BZOJ2208 [Jsoi2010]连通数[缩点/Floyd传递闭包+bitset优化]

    显然并不能直接dfs,因为$m$会非常大,复杂度就是$O(mn)$: 这题有三种做法,都用到了bitset的优化.第二种算是一个意外的收获,之前没想到竟然还有这种神仙操作.. 方法一:缩点+DAG上b ...

  10. BZOJ 1283: 序列 (最大费用流)

    题意 有n个正整数,要选取里面的一些数,在保证每m个连续的数中最多选k个的情况下,使得得到的值最大. 分析 我们可以把问题先转化为选k次,每一次每m个数只能选一个.那么根据贪心的策略,每m个里一定会选 ...