Celery用户指引-------------Tasks

Tasks就是组成Celery应用程序的积木。

Tasks有双重角色,它在任务被调用时(发送消息)和worker接收到任务消息时都会起作用。

每个任务都有一个唯一的名字,这个名字会关联到对应的消息。这样worker就能知道要执行哪个函数。

一个任务消息直到被一个worker任务确认之后才会消失。

一个worker可以预先存储许多消息,尽管这个worker可能因为掉电都原因挂掉,但是这些消息会被重新递送给另一人worker.

理想的任务函数应该是“幂等的”,这意味着这个函数以相同的参数无论调用多少次都会产生相同的结果。

因为worker没有办法判断一个task是否是幂等的,所以在默认情况下,它会在执行任务之前先对消息进行确认,所以一个已经启动的任务不会被重复执行。

如果你的任务是幂等的,你可以设置acks_late选项,这样,worker就会在任务返回之后再进行确认。更多详细的介绍请查看:Should I use retry or acks_late?.


在这一节,你将学习到如何定义一个任务。



基础

你可以通过task()装饰器在任何可调用的对象上轻松地创建一个任务。

from .models import User

@app.task
def create_user(username, password):
    User.objects.create(username=username, password=password)
在创建任务时有许多可以设置的选项,你可以通过关键字参数来指定它们:

@app.task(serializer='json')
def create_user(username, password):
    User.objects.create(username=username, password=password)

使用多个装饰器

当你与task()装饰器组合使用多个装饰器时,你需要保证task()装饰器被最后使用。在python中,主意味着你需要把task()装饰器放在列表最前面:

@app.task
@decorator2
@decorator1
def add(x, y):
    return x + y


怎样导入task装饰器,"app"又是什么?

在你的celery实例中可以使用task()装饰器,如果你还不明白,请查看"使用Celery的第一步"

如果你正在使用Django或者还在使用老的基于模块的API,你可以向下面这样导入task装饰器:

from celery import task

@task
def add(x, y):
    return x + y

绑定任务

一个任务被绑定意味着这个任务的第一个参数是celery app实例(即self),和python中的绑定方法一样:

logger = get_task_logger(__name__)

@task(bind=True)
def add(self, x, y):
    logger.info(self.request.id)
绑定任务用于尝试重新执行任务(使用app.Task.retry()),绑定了任务就可以访问当前请求的任务信息和任何你添加到指定任务基类中的方法。



任务继承

task装饰器的"base"关键字参数用于指定任务的基类:

import celery

class MyTask(celery.Task):

    def on_failure(self, exc, task_id, args, kwargs, einfo):
        print('{0!r} failed: {1!r}'.format(task_id, exc)

@task(base=MyTask)
def add(x, y):
    raise KeyError()

名字

每个任务都必须拥有一个唯一的名字,如果你没有指定"name"关键字参数,将会用函数名产生一个名字。

>>> @app.task(name='sum-of-two-numbers')
>>> def add(x, y):
...     return x + y

>>> add.name
'sum-of-two-numbers'
最佳实践是使用模块名作为一个命名空间,这样任务名就不会与另外一个模块中已经存在的同名任务冲突:

>>> @app.task(name='tasks.add')
>>> def add(x, y):
...     return x + y
你可以通过任务的name属性来查看任务的名字:

>>> add.name
'tasks.add'
如果任务所在的模块名为"task.py",那么任务的名字将会像如下这样生成:

tasks.py:

@app.task
def add(x, y):
    return x + y

>>> from tasks import add
>>> add.name
'tasks.add'

自动命名和相对导入

"相对导入"和"自动名称生成"在一起工作的不是很好,所以如果你使用了"相对导入",就需要显式地指明名字。

比如,如果客户端导入模块"myapp.tasks"作为".tasks",然后worker导入模块作为“myapp.tasks”,这样自动生成的名字就不能够正确匹配,worker就会抛出一个 NotRegistered 异常。

同样的情况在Django中也会发生,当你在INSTALLED_APPS中使用了"project.myapp"形式的名字:

INSTALLED_APPS = ['project.myapp']
如果你的app安装在project.myapp下,这样你的tasks模块将会以"project.myapp.tasks"形式导入,所以你必须保证总是以同样的名字导入tasks:

>>> from project.myapp.tasks import mytask   # << GOOD

>>> from myapp.tasks import mytask    # << BAD!!!
下面是第二个例子,client和worker以不同的名称导入了tasks模块进而造成了task被命名为不同的名字:

>>> from project.myapp.tasks import mytask
>>> mytask.name
'project.myapp.tasks.mytask'

>>> from myapp.tasks import mytask
>>> mytask.name
'myapp.tasks.mytask'
基于以上原因,你必须以相同的方式导入模块,这在python中也是一个好的实践。

相似地,你不要使用老的相对导入方式:

from module import foo   # BAD!

from proj.module import foo  # GOOD!
新的相对导入方式是可以的:

from .module import foo  # GOOD!
如果你正在使用的Celery工程中已经广泛使用了这样的导入方式,你也没有时间重构这些代码,那么你可以考虑显式地指定任务的名称而不是依赖于自动命名:

@task(name='proj.tasks.add')
def add(x, y):
    return x + y

改变自动命名行为

4.0版本中的新特性

在有些情况下,自动命名机制不太合适。比如你有很多任务在不同的模块之中:

project/
       /__init__.py
       /celery.py
       /moduleA/
               /__init__.py
               /tasks.py
       /moduleB/
               /__init__.py
               /tasks.py
在默认的自动命名机制下,每个任务的名字会形如"moduleA.tasks.taskA","moduleB.tasks.taskB"等等。你也许不希望任务名称中都出现tasks。

为了去掉每个任务中的tasks,你可以显式地指定每个任务的名字,或者通过覆盖app.gen_task_name()方法来改变默认命名的行为。

from celery import Celery

class MyCelery(Celery):

    def gen_task_name(self, name, module):
        if module.endswith('.tasks'):
            module = module[:-6]
        return super(MyCelery, self).gen_task_name(name, module)

app = MyCelery('main')
这样,每个任务中就不会有tasks了。任务的名称将会变为"moduleA.taskA","moduleB.taskB"。

警告:

你必须确认你定义的app.gen_task_name是一个纯函数,即对于相同的输入它必须产生同样的输出。



上下文

request包含了当前正在执行任务的信息和状态。

request定义了如下属性:

id:正在执行的任务的唯一ID.
group:任务的唯一组ID,如果当前任务属于这个组.
chord:The unique id of the chord this task belongs to (if the task is part of the header).
args:Positional arguments.
kwargs:Keyword arguments.
retries:How many times the current task has been retried. An integer starting at 0.
is_eager:Set to True if the task is executed locally in the client, and not by a worker.
eta:The original ETA of the task (if any). This is in UTC time (depending on the enable_utc setting).
expires:The original expiry time of the task (if any). This is in UTC time (depending on the enable_utc setting).
logfile:The file the worker logs to. See Logging.
loglevel:The current log level used.
hostname:Node name of the worker instance executing the task.
delivery_info:Additional message delivery information. This is a mapping containing the exchange and routing key used to deliver this task. Used by e.g.retry() to resend the task to the same destination queue. Availability of keys in this dict depends on the message broker used.
called_directly:
 This flag is set to true if the task was not executed by the worker.
callbacks:A list of signatures to be called if this task returns successfully.
errback:A list of signatures to be called if this task fails.
utc:Set to true the caller has UTC enabled (enable_utc).

New in version 3.1.

headers:Mapping of message headers (may be None).
reply_to:Where to send reply to (queue name).
correlation_id:Usually the same as the task id, often used in amqp to keep track of what a reply is for.

下面是一task访问上下文信息的例子:

@app.task(bind=True)
def dump_context(self, x, y):
    print('Executing task id {0.id}, args: {0.args!r} kwargs: {0.kwargs!r}'.format(
            self.request))

bind关键字参数如上面所说,这个任务将会成为一个绑定任务,因此可以访问app实例中的属性。


日志

worker会自动为你设置日志,你也可以手动配置日志。

你可以使用一个名为“celery.task”的特定logger,你可以从这个logger继承,这样你就能够自动在log里记录任务名称和taks id.

最佳实践是在模块的顶层,为你的所有任务创建一个共用的logger。

from celery.utils.log import get_task_logger

logger = get_task_logger(__name__)

@app.task
def add(x, y):
    logger.info('Adding {0} + {1}'.format(x, y))
    return x + y
Celery使用标准的python logger库,相关文档可以查看: logging

你也可以使用print(),所有写到标准输出/标准错误输出的信息都会被重定向到日志系统(你也可以关闭这个功能,参见 worker_redirect_stdouts

注意:

如果你在任务或者任务模块的一些地方创建了一个logging实例,worker不会更新重定向。

如果你想重定向sys.stdout和sys.stderr到一个指定的logger,你必须手工开启:

import sys

logger = get_task_logger(__name__)

@app.task(bind=True)
def add(self, x, y):
    old_outs = sys.stdout, sys.stderr
    rlevel = self.app.conf.worker_redirect_stdouts_level
    try:
        self.app.log.redirect_stdouts_to_logger(logger, rlevel)
        print('Adding {0} + {1}'.format(x, y))
        return x + y
    finally:
        sys.stdout, sys.stderr = old_outs


隐藏参数中的敏感信息:

4.0中的新特性

当你使用 task_protocol 2或者更高版本的任务协议时,你可以通过调用时的argsrepr和kwargsrepr参数来覆盖位置参数和关键字参数在log或者监控事件中的显示形式。

>>> add.apply_async((2, 3), argsrepr='(<secret-x>, <secret-y>)')

>>> charge.s(account, card='1234 5678 1234 5678').set(
...     kwargsrepr=repr({'card': '**** **** **** 5678'})
... ).delay()
警告:

对于任何能够从“中间人”中读取你的任务消息的人来说,他们也可以访问任务中的敏感信息。

所以你可能需要对敏感信息进行加密,对于上面例子中的卡号和密码你可以以密文形式存储,然后在任务代码里进行解密和使用。


重试

retry()方法可以用来重新执行任务,比如发生了可以恢复的错误。

当你调用retry重新发送一个消息时,将会使用相同的任务id,而且它将会被递送到和之前任务相同的队列中。

一个任务被重试执行也会被记录为一个状态,所以你也可以通过result实例来跟踪任务的进程。查看:States

下面是一个使用retry的例子:

@app.task(bind=True)
def send_twitter_status(self, oauth, tweet):
    try:
        twitter = Twitter(oauth)
        twitter.update_status(tweet)
    except (Twitter.FailWhaleError, Twitter.LoginError) as exc:
        raise self.retry(exc=exc)
注意:

retry()调用会抛出一个异常,所以retry调用之后的代码不会被执行。

这个异常是Retry异常,这不会被认为是一个错误,相反,这对worker意味着任务被重新执行了。如果设置了result backend,也会存储正确的状态。

通过task装饰器的bind关键字参数你可以访问self(task类型的实例)。

"exc"用来向日志传递所需的异常信息,这需要存储了任务结果。无论是异常还是回溯栈都可以在任务状态中获取到(在开启结果后台的情况下)。

如果任务有最大尝试次数,当达到最大尝试次数后,当前的异常会再次被抛出。但是在下面情况下不会这样:

a.没有给出"exc"参数

  在这种情况下,会抛出MaxRetriesExceededError异常。

b.没有异常

如果没有发生过异常,exc参数指定的异常将会被抛出

self.retry(exc=Twitter.LoginError())


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值