Как реализовать autoretry для задач сельдерея

В сельдерей, вы можете retry любая задача в случае исключения. Вы можете сделать это так:

@task(max_retries=5)
def div(a, b):
    try:
        return a / b
    except ZeroDivisionError, exc:
        raise div.retry(exc=exc)

в этом случае, если вы хотите разделить на ноль, задача будет удалена пять раз. Но вы должны проверить наличие ошибок в коде явно. Задача не будет удалена, если вы пропустите try-except заблокировать.

Я хочу, чтобы мои функции, чтобы выглядеть как:

@celery.task(autoretry_on=ZeroDivisionError, max_retries=5)
def div(a, b):
    return a / b

4 ответов


Я искал эту проблему некоторое время, но нашел только эта функция запрос.

Я решаю написать свой собственный декоратор для выполнения авто-повторов:

def task_autoretry(*args_task, **kwargs_task):
    def real_decorator(func):
        @task(*args_task, **kwargs_task)
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                func(*args, **kwargs)
            except kwargs_task.get('autoretry_on', Exception), exc:
                wrapper.retry(exc=exc)
        return wrapper
    return real_decorator

С этим декоратором я могу переписать свою предыдущую задачу:

@task_autoretry(autoretry_on=ZeroDivisionError, max_retries=5)
def div(a, b):
    return a / b

сельдерей (начиная с версии 4.0) имеет именно то, что вы искали:

@app.task(autoretry_for=(SomeException,))
def my_task():
    ...

см.: http://docs.celeryproject.org/en/latest/userguide/tasks.html#automatic-retry-for-known-exceptions


я модифицировал ваш ответ для работы с существующим API сельдерея (в настоящее время 3.1.17)

class MyCelery(Celery):
    def task(self, *args_task, **opts_task):
        def real_decorator(func):
            sup = super(MyCelery, self).task

            @sup(*args_task, **opts_task)
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                try:
                    func(*args, **kwargs)
                except opts_task.get('autoretry_on', Exception) as exc:
                    logger.info('Yo! We did it!')
                    wrapper.retry(exc=exc, args=args, kwargs=kwargs)
            return wrapper
        return real_decorator

тогда, в ваши задачи

app = MyCelery()
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

@app.task(autoretry_on=Exception)
def mytask():
    raise Exception('Retrying!')

Это позволяет добавить функциональность autoretry_on к вашим задачам без использования отдельного декоратора для определения задач.


вот улучшенная версия существующих ответов.

это полностью реализует поведение сельдерея 4.2 (as документы), но для сельдерей 3.1.25.

Он также не нарушает различные формы декоратора задач (с/без скобок) и возвращает/поднимает правильно.

import functools
import random

from celery.app.base import Celery as BaseCelery


def get_exponential_backoff_interval(factor, retries, maximum, full_jitter=False):
    """
    Calculate the exponential backoff wait time.

    (taken from Celery 4 `celery/utils/time.py`)
    """
    # Will be zero if factor equals 0
    countdown = factor * (2 ** retries)
    # Full jitter according to
    # https://www.awsarchitectureblog.com/2015/03/backoff.html
    if full_jitter:
        countdown = random.randrange(countdown + 1)
    # Adjust according to maximum wait time and account for negative values.
    return max(0, min(maximum, countdown))


class Celery(BaseCelery):

    def task(self, *args, **opts):
        """
        Overridden to add a back-port of Celery 4's `autoretry_for` task args.
        """
        super_method = super(Celery, self).task

        def inner_create_task_cls(*args_task, **opts_task):
            # http://docs.celeryproject.org/en/latest/userguide/tasks.html#Task.autoretry_for
            autoretry_for = tuple(opts_task.get('autoretry_for', ()))  # Tuple[Type[Exception], ...]
            retry_backoff = int(opts_task.get('retry_backoff', False))  # multiplier, default if True: 1
            retry_backoff_max = int(opts_task.get('retry_backoff_max', 600))  # seconds
            retry_jitter = opts_task.get('retry_jitter', True)  # bool
            retry_kwargs = opts_task.get('retry_kwargs', {})

            def real_decorator(func):
                @super_method(*args_task, **opts_task)
                @functools.wraps(func)
                def wrapper(*func_args, **func_kwargs):
                    try:
                        return func(*func_args, **func_kwargs)
                    except autoretry_for as exc:
                        if retry_backoff:
                            retry_kwargs['countdown'] = get_exponential_backoff_interval(
                                factor=retry_backoff,
                                retries=wrapper.request.retries,
                                maximum=retry_backoff_max,
                                full_jitter=retry_jitter,
                            )
                        raise wrapper.retry(exc=exc, **retry_kwargs)
                return wrapper
            return real_decorator

        # handle both `@task` and `@task(...)` decorator forms
        if len(args) == 1:
            if callable(args[0]):
                return inner_create_task_cls(**opts)(*args)
            raise TypeError('argument 1 to @task() must be a callable')
        if args:
            raise TypeError(
                '@task() takes exactly 1 argument ({0} given)'.format(
                    sum([len(args), len(opts)])))
        return inner_create_task_cls(**opts)

Я также написал некоторые модульные тесты для этого, как я использую его в своем проекте.

их можно найти в в этом суть но обратите внимание, что они не легко запускать-рассматривайте больше как документацию о том, как работает вышеуказанная функция (и проверку того, что она работает правильно).