Celery: Celery не выполняет задачи на основе классов

Созданный на 7 янв. 2017  ·  11Комментарии  ·  Источник: celery/celery

У меня возникли проблемы с новой версией Celery. Он не обнаружит задачи на основе классов.

Вот моя конфигурация и версии:

Версии

  • Джанго 1.10
  • Сельдерей: 4.0.2

Установлен сельдерей с pip install -U celery затем установлен redis, pip install redis .

# settings.py
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'

# celery.py
# coding: utf-8
"""Configuração inicial do Celery"""
from __future__ import absolute_import
from django.conf import settings
import os
from celery import Celery
from scout.settings.utils import get_env_variable
try:
    setts = get_env_variable('DJANGO_SETTINGS_MODULE')
except:
    setts = 'scout.settings.local'
os.environ.setdefault('DJANGO_SETTINGS_MODULE', setts)

app = Celery('scout')
app.config_from_object('django.conf:settings', namespace='CELERY')
# i have tried this line below with different setups. using lambda, without anything, etc.
app.autodiscover_tasks([a for a in settings.INSTALLED_APPS])

Задания:

### workflow/tasks.py ###

class BaseTask(Task):

    ignore_result = False
    validation_class = ''
    name = ''
    description = ''

    def run(self, *args, **kwargs):
        controller_id = kwargs.pop('cid', None)
        next_id = kwargs.pop('nsi', None)
        self._load_data(controller_id, next_id)
        return self._run()

    def _load_data(self, controller_id, next_id):

        self.controller = StateController.objects.get(id=controller_id)
        self.previous = self.controller.current_state
        self.next = State.objects.get(id=next_id)

    def _run(self):

        return True

### addresses/tasks.py ###

# coding: utf-8
from workflow.tasks import BaseTask
from celery import current_app


@current_app.task
def foo(x, y):
    return x + y


class AddressMatching(BaseTask):
    name = 'Address Matching'
    description = '''Matches address from sources A and B and constructs
a list of Address Matches for other analysis and manual review.'''
    public = True

    def _run(self, *args, **kwargs):
        print 'address matching'
        return True


class CanonicalMatching(BaseTask):
    name = 'Canonical Matching'
    description = '''Matches addresses from Address Matches
with canonical source of addresses. These canonical sources might be
addresses from official government sources of ADDRESSES (not POIs)
and other sources, like OpenStreetMap'''
    public = True

    def _run(self, *args, **kwargs):
        print 'canon matching'
        return True

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

Действия по воспроизведению

  1. Настройте проект примерно так:
  2. Выполнить рабочий celery -A scout worker -l info

Выход:

[tasks]
  . addresses.tasks.foo
  . scout.tasks.email_async

[2017-01-07 21:00:28,811: INFO/MainProcess] Connected to redis://localhost:6379/0
[2017-01-07 21:00:28,819: INFO/MainProcess] mingle: searching for neighbors
[2017-01-07 21:00:29,833: INFO/MainProcess] mingle: all alone
[2017-01-07 21:00:29,845: WARNING/MainProcess] /home/george/projetos/.virtualenvs/scout/local/lib/python2.7/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!
  warnings.warn('Using settings.DEBUG leads to a memory leak, never '
[2017-01-07 21:00:29,845: INFO/MainProcess] celery<strong i="26">@winterfell</strong> ready.

Ожидаемое поведение

Задачи на основе класса также следует подбирать сельдереем. Когда я пытаюсь делегировать задачу Celery, он отказывается от нее, потому что она не зарегистрирована. Обратите внимание, что обычная задача, основанная на функциях , правильно подбирается сельдереем .

[2017-01-07 21:01:31,922: ERROR/MainProcess] Received unregistered task of type 'Address Matching'.
The message has been ignored and discarded.

Did you remember to import the module containing this task?
Or maybe you're using relative imports?

Please see
http://docs.celeryq.org/en/latest/internals/protocol.html
for more information.

The full contents of the message body was:
'[[], {}, {"chord": null, "callbacks": null, "errbacks": null, "chain": null}]' (77b)
Traceback (most recent call last):
  File "/home/george/projetos/.virtualenvs/scout/local/lib/python2.7/site-packages/celery/worker/consumer/consumer.py", line 559, in on_task_received
    strategy = strategies[type_]
KeyError: 'Address Matching'

Самый полезный комментарий

Без автообнаружения. Я регистрируюсь после того, как определю класс. Вот пример задачи на основе классов, которую я использую, где я использую fragments() в качестве полиморфного метода и определяю список для каждого класса.

Я использую собственный маршрутизатор (карты между lib.task и myapp), и мой imports содержит: 'lib.task.extract.discovery', .

from lib.task.extract.base import ExtractTask
from myapp.celery import celery_app


class ExtractDiscoveryTask(ExtractTask):
    name = 'myapp.discovery.extract'

    def fragments(self, site):
        return site.discovery_fragments

celery_app.tasks.register(ExtractDiscoveryTask())

Все 11 Комментарий

Я считаю, что вам нужно использовать current_app.Task в качестве базового класса, чтобы зарегистрировать его.

@ddemid привет! Спасибо за помощь. но ничего хорошего :(

Я пробовал две вещи:

  1. Унаследовано от базового класса Task одной из моих конкретных задач;
  2. Изменена BaseTask для наследования от current_app.Task;

Вот так:

# coding: utf-8
from celery import Task
from workflow.tasks import BaseTask
from celery import current_app


@current_app.task
def foo(x, y):
    return x + y


@current_app.task
def address_matching(x,y):
    pass


class AddressMatching(Task):
    name = 'Address Matching'
    description = '''Matches address from sources A and B and constructs
a list of Address Matches for other analysis and manual review.'''
    public = True

    def _run(self, *args, **kwargs):
        print 'address matching'
        return True

class BaseTask(current_app.Task):

    ignore_result = False
    validation_class = ''
    name = ''
    description = ''

    def run(self, *args, **kwargs):
        controller_id = kwargs.pop('cid', None)
        next_id = kwargs.pop('nsi', None)
        self._load_data(controller_id, next_id)
        return self._run()

    def _load_data(self, controller_id, next_id):

        self.controller = StateController.objects.get(id=controller_id)
        self.previous = self.controller.current_state
        self.next = State.objects.get(id=next_id)

    def _run(self):

        return True

Однако ни одна из этих задач не решена. Поднятые задачи основаны на функциях.

[tasks]
  . addresses.tasks.address_matching
  . addresses.tasks.foo
  . scout.tasks.email_async

Просто для пояснения: мне нужно, чтобы это были задачи на основе классов, потому что в настоящее время есть некоторое поведение обнаружения, которое мне нужно поддерживать.

Пользователь может выбирать эти задачи, и у них есть некоторые «особые» атрибуты, которые мне нужно поддерживать. Validation_class, имя и описание используются Django Rest Framework.

У меня была такая же проблема. Вам нужно будет явно зарегистрировать каждую задачу. Например, после определения AddressMatching и CanonicalMatching вызовите:

current_app.tasks.register(AddressMatching())
current_app.tasks.register(CanonicalMatching())

См. Комментарий @ask к # 3645.

@rickwargo где вы их регистрируете? Как вы настроили CELERY_IMPORTS или автообнаружение?

Пытался пройти регистрацию сам, но без кубика.

Без автообнаружения. Я регистрируюсь после того, как определю класс. Вот пример задачи на основе классов, которую я использую, где я использую fragments() в качестве полиморфного метода и определяю список для каждого класса.

Я использую собственный маршрутизатор (карты между lib.task и myapp), и мой imports содержит: 'lib.task.extract.discovery', .

from lib.task.extract.base import ExtractTask
from myapp.celery import celery_app


class ExtractDiscoveryTask(ExtractTask):
    name = 'myapp.discovery.extract'

    def fragments(self, site):
        return site.discovery_fragments

celery_app.tasks.register(ExtractDiscoveryTask())

Ой, вот мы и разговариваем: D.

Мне это удалось. У нас уже был загрузчик, который проверяет все эти дочерние задачи (BaseTask). Поэтому я просто добавил к этому методу новый регистр.

Существует множество шаблонов для добавления этих задач к моделям в БД, но что-то подобное должно работать для глобального реестра.

Проверь это:

class AvailableTaskLoader(object):

    def _get_subclasses(self):
        task_dict = {}
        for app in settings.INSTALLED_APPS:
            try:
                mod = __import__('%s.%s' % (app, 'tasks'))
            except:
                continue
            members = inspect.getmembers(mod.tasks, predicate=lambda x: inspect.isclass(x) and issubclass(x, BaseTask))
            for m in members:
                # {'foo.bar.Task': 'class <foo.bar.Task>'}
                task_dict['{0}.{1}'.format(m[1].__module__, m[0])] = m[1]
        return task_dict

    def load(self):
        subcls = self._get_subclasses()
        for cls_name, cls in subcls.items():

            current_app.tasks.register(cls)

            if not hasattr(cls, 'public') or not cls.public:
                continue
            full_name = '{0}.{1}'.format(cls.__module__, cls.__name__)
            name = cls.name if cls.name else cls.__name__
            description = cls.description if cls.description else cls.__doc__
            try:
                obj, created = AvailableTask.objects.get_or_create(name=name,
                                                                   klass=full_name,
                                                                   description=description)
                logger.info('AvailableTask %s created successfully.', full_name)
            except Exception as ex:
                logger.warning('Error while creating %s. %s',
                               full_name,
                               ex.message)

        self._prune()

    def _prune(self):
        '''removes all the unecessary tasks'''
        tasks = set([member for member in self._get_subclasses().keys()])
        existing = set(AvailableTask.objects.all().values_list('klass', flat=True))
        stale = existing - tasks
        for s in stale:
            at = AvailableTask.objects.get(klass=s)
            at.transition_tasks.all().delete()
            at.delete()

Вот начальный результат сельдерея:

[tasks]
  . Address Matching
  . Base Task
  . Canonical Matching
  . Change State
  . scout.tasks.email_async

: clap:: clap:: clap:

@ask, может, стоит добавить это в документы? Перед переходом на 4.0.2 (не помню, какой версией я пользовался) эти задачи находились автоматически.

Пожалуйста, добавьте это в документы; Сегодня я потерял много времени при обновлении до сельдерея 4.0.2.

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

Может не по теме:
Есть несколько блогов, которые пишут о лучших практиках с задачами на основе классов, но похоже, что Celery 4 не поддерживает их? Я нашел это в документах:
«Это изменение также означает, что абстрактный атрибут задачи больше не имеет никакого эффекта». здесь
Должны ли мы прекратить использовать задачи на основе (абстрактных) классов?

На следующей неделе у меня будет время, чтобы написать короткое руководство или написать об этом главу в документации.

После того, как я повозился с этой кучей, мне стало ясно, что вы больше не собираетесь использовать задачи на основе классов. Вместо этого всегда используйте подход декоратора.

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

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

Возможно, # 3874 имеет отношение.

закрытие в пользу https://github.com/celery/celery/issues/3874 надеюсь, что это исправлено

Была ли эта страница полезной?
0 / 5 - 0 рейтинги