趁热打铁,学习一下 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 的理解主要就是创建和修改,以及序列化和反序列化这部分代码,剩下的没有介绍的方法难度都不大,感兴趣的可以自己阅读一下~
本文深入探讨了APScheduler中的Job类,详细分析了Job的创建、修改以及序列化和反序列化过程。重点讲解了_job_init_、_modify_函数,包括参数校验、可执行函数的序列化与反序列化、任务状态和配置的设置。同时,提到了__getstate__和__setstate__在对象序列化与反序列化中的作用。文章还介绍了func参数的解析,特别是对字符串形式的函数引用和可调用对象的处理。
3007

被折叠的 条评论
为什么被折叠?



