APScheduler 源码阅读(二) job

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

分析源码主要还是针对 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._schedulerself._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_selfim_class 主要是针对 Python 2.x版本 的内容,实际上 Python 3.x 已经没有这些 bound methodunbound 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 的理解主要就是创建和修改,以及序列化和反序列化这部分代码,剩下的没有介绍的方法难度都不大,感兴趣的可以自己阅读一下~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

会偷懒的程序猿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值