趁热打铁,学习一下 APScheduler
的 python
的源码,很好奇任务调度控制的实现。
分析源码主要还是针对 APScheduler
下的几个关键的模块
events
事件executors
执行器job
任务jobstores
任务存储triggers
触发器schedulers
调度程序
这一篇主要瞅瞅 job
事件
Job
记录自己的触发条件 triggers
, 记录自己的所属的任务存储 jobstores
, 记录自己交给谁执行 executors
, 记录由谁来调度 schedulers
以及一些任务自身的一些例如任务的唯一标识 job_id
,执行时允许的时间误差 misfire_grace_time
等等, 大概就下面这些信息
'id': self.id,
'func': self.func_ref,
'trigger': self.trigger,
'executor': self.executor,
'args': args,
'kwargs': self.kwargs,
'name': self.name,
'misfire_grace_time': self.misfire_grace_time,
'coalesce': self.coalesce,
'max_instances': self.max_instances,
'next_run_time': self.next_run_time
简单点说,Job
类包含了调度程序调用时需要的所有的配置参数,以及任务当前的状态和所属的调度程序
因为 Job
只是信息的保存,但是例如 Job
的暂停恢复,实际上调度程序 schedulers
来控制的,所以源码中对 Job
提供的例如 修改,暂停,恢复 等等操作实际上都是通过调用 schedulers
的接口来实现的
Job
源码中需要了解的部分实际上只有 创建 和 修改 2个函数,以及一些 python
定义的特殊方法( __eq__
, __str__
等等) 的重载
初始化 __init__
def __init__(self, scheduler, id=None, **kwargs):
super(Job, self).__init__()
self._scheduler = scheduler
self._jobstore_alias = None
self._modify(id=id or uuid4().hex, **kwargs)
很清楚的看出,创建时的对 Job
成员对象的设置,也是通过修改 _modify
这个函数设置的,主要还是分析一下 _modify
代码
修改 _modify
schedulers
中其实对输入参数做了细致的定义,以及为它们初始化做了对应的初始化,大致如下,具体等介绍 schedulers
时展开
add_job(self, func, trigger=None, args=None, kwargs=None, id=None, name=None,
misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined,
next_run_time=undefined, jobstore='default', executor='default',
replace_existing=False, **trigger_args)
而 _modify
主要就是对传入参数进行参数的校验,并且需要考虑创建和修改的区别
首先解释一下创建
配合 __init__()
中的代码,在执行 _modify
之前实际上的成员对象只有 self._scheduler
和 self._jobstore_alias
, 所以创建的时候其他成员对象都是在解析输入后传入 approved = {}
中,并通过下面这部分的代码给对象的属性赋值,若属性不存在,先创建再赋值。
for key, value in six.iteritems(approved):
setattr(self, key, value)
而修改时,部分参数是不能修改的例如 id
if 'id' in changes:
value = changes.pop('id')
# id 必须是 string
if not isinstance(value, six.string_types):
raise TypeError("id must be a nonempty string")
# 创建时不存在 id 属性,修改的时候不允许修改
if hasattr(self, 'id'):
raise ValueError('The job ID may not be changed')
approved['id'] = value
此外还有一部分需要注意, func
的解析部分需要注意
if 'func' in changes or 'args' in changes or 'kwargs' in changes:
func = changes.pop('func') if 'func' in changes else self.func
args = changes.pop('args') if 'args' in changes else self.args
kwargs = changes.pop('kwargs') if 'kwargs' in changes else self.kwargs
if isinstance(func, six.string_types):
func_ref = func
func = ref_to_obj(func)
elif callable(func):
try:
func_ref = obj_to_ref(func)
except ValueError:
# If this happens, this Job won't be serializable
func_ref = None
else:
raise TypeError('func must be a callable or a textual reference to one')
if not hasattr(self, 'name') and changes.get('name', None) is None:
changes['name'] = get_callable_name(func)
if isinstance(args, six.string_types) or not isinstance(args, Iterable):
raise TypeError('args must be a non-string iterable')
if isinstance(kwargs, six.string_types) or not isinstance(kwargs, Mapping):
raise TypeError('kwargs must be a dict-like object')
check_callable_args(func, args, kwargs)
approved['func'] = func
approved['func_ref'] = func_ref
approved['args'] = args
approved['kwargs'] = kwargs
可执行函数如果以字符串的形式传入,所以需要通过 ref_to_obj
反序列化成可执行对象,当然直接传入一个可执行的函数也可以,通过 callable()
(对于函数、方法、lambda 函式、 类以及实现了 __call__ 方法的类实例, 它都返回 True
) 来测试是否可以调用,之后还需要检测形参下是否支持 check_callable_args(func, args, kwargs)
反序列化 ref_to_obj
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, fromlist=[rest])
except ImportError:
raise LookupError('Error resolving reference %s: could not import module' % ref)
try:
for name in rest.split('.'):
obj = getattr(obj, name)
return obj
except Exception:
raise LookupError('Error resolving reference %s: error looking up object' % ref)
当输入为字符串时,通过 :
切割成2部分,第一部分是模块名,通过 __import__
导入第二部分的指定接口,最后通过 getattr
一层层获取到最后的目标接口,例如:
# testfunc 模块下 test 类里 task 函数
if __name__ == '__main__':
scheduler = BlockingScheduler()
scheduler.add_job(func="testfunc:test.task", args=('定时任务',), trigger='cron', second='*/5', id="定时任务")
scheduler.start()
序列化 obj_to_ref
def obj_to_ref(obj):
"""
Returns the path to the given callable.
:rtype: str
:raises TypeError: if the given object is not callable
:raises ValueError: if the given object is a :class:`~functools.partial`, lambda or a nested
function
"""
if isinstance(obj, partial):
raise ValueError('Cannot create a reference to a partial()')
name = get_callable_name(obj)
if '<lambda>' in name:
raise ValueError('Cannot create a reference to a lambda')
if '<locals>' in name:
raise ValueError('Cannot create a reference to a nested function')
if ismethod(obj):
if hasattr(obj, 'im_self') and obj.im_self:
# bound method
module = obj.im_self.__module__
elif hasattr(obj, 'im_class') and obj.im_class:
# unbound method
module = obj.im_class.__module__
else:
module = obj.__module__
else:
module = obj.__module__
return '%s:%s' % (module, name)
partial
用于部分函数应用程序,该应用程序 “冻结” 函数参数和/或关键字的一部分,从而生成一个带有简化签名的新对象,看一下下面的代码,partial
是固定了 func
的部分参数,并生成一个新的签名,它是不支持序列化的
from functools import partial
def func(x, y):
return x + y
f1 = partial(func, y=4) # 固定 y=4
print(f1(1)) # 5
通过 get_callable_name
就是获取函数最佳显示名称,对于 Python3.3+
版本,可以直接使用 func.__qualname__
来获取,低于这个版本的获取有点麻烦,感兴趣的可以自己看一下源码
<lambda>
和 <locals>(嵌套函数)
也是没法序列化成 模块名 : 函数
,这意味着使用非内存模式的 job_store
,因为无法序列化,所以这3种类型需要额外注意
ismethod(obj)
用来判断对象是函数还是方法,在 python
中这2者是有一点出入的, 定义在类外面的是函数,定义在类里面的,跟类绑定的是方法,而后面的 im_self
和 im_class
主要是针对 Python 2.x版本
的内容,实际上 Python 3.x
已经没有这些 bound method
和 unbound method
的概念了
而关于这方面的介绍,可以看一下这位大佬的介绍: https://www.jianshu.com/p/a497f742ddd4
modify
中解析 func
部分就解释完了 , check_callable_args(func, args, kwargs)
主要就是确保可以使用给定的参数调用给定的 func
。
剩余字段的解析都很简单,从 changes
中取出,根据实际情况数值校验,然后扔到 approved
中,最后在通过对 approved
的迭代 setattr
, 例如 misfire_grace_time
:
if 'misfire_grace_time' in changes:
value = changes.pop('misfire_grace_time')
if value is not None and (not isinstance(value, six.integer_types) or value <= 0):
raise TypeError('misfire_grace_time must be either None or a positive integer')
approved['misfire_grace_time'] = value
for key, value in six.iteritems(approved):
setattr(self, key, value)
__getstate__
和 __setstate__
__getstate__
与 __setstate__
两个方法分别用于对象的序列化与反序列化
在序列化时, __getstate__
可以指定将那些信息记录下来, 而 __setstate__
指明如何利用已记录的信息
这一部分的代码很简单,大致就是利用 _modify
中解析得到的属性,生成一个字典,或者用字典对 Job
的属性进行设置
def __getstate__(self):
if not self.func_ref:
raise ValueError(
'This Job cannot be serialized since the reference to its callable (%r) could not '
'be determined. Consider giving a textual reference (module:function name) '
'instead.' % (self.func,))
if ismethod(self.func) and not isclass(self.func.__self__):
args = (self.func.__self__,) + tuple(self.args)
else:
args = self.args
return {
'version': 1,
'id': self.id,
'func': self.func_ref,
'trigger': self.trigger,
'executor': self.executor,
'args': args,
'kwargs': self.kwargs,
'name': self.name,
'misfire_grace_time': self.misfire_grace_time,
'coalesce': self.coalesce,
'max_instances': self.max_instances,
'next_run_time': self.next_run_time
}
def __setstate__(self, state):
if state.get('version', 1) > 1:
raise ValueError('Job has version %s, but only version 1 can be handled' %
state['version'])
self.id = state['id']
self.func_ref = state['func']
self.func = ref_to_obj(self.func_ref)
self.trigger = state['trigger']
self.executor = state['executor']
self.args = state['args']
self.kwargs = state['kwargs']
self.name = state['name']
self.misfire_grace_time = state['misfire_grace_time']
self.coalesce = state['coalesce']
self.max_instances = state['max_instances']
self.next_run_time = state['next_run_time']
总结
对 Job
的理解主要就是创建和修改,以及序列化和反序列化这部分代码,剩下的没有介绍的方法难度都不大,感兴趣的可以自己阅读一下~