from __future__ import absolute_import, unicode_literals | from __future__ import absolute_import, unicode_literals | ||||
# This will make sure celery is always imported when | |||||
# Django starts so that shared_task will use this app. | |||||
from .celeryapp import app as celery_app | |||||
__all__ = ['celery_app'] |
from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||
from .models import Post, CustomUser | from .models import Post, CustomUser | ||||
from .models import ScheduledReport, ReportRecipient, ScheduledReportGroup | |||||
from .forms import ScheduledReportForm | |||||
class CustomUserInline(admin.StackedInline): | class CustomUserInline(admin.StackedInline): | ||||
admin.site.register(Post) | admin.site.register(Post) | ||||
class ReportRecipientAdmin(admin.TabularInline): | |||||
model = ReportRecipient | |||||
class ScheduledReportAdmin(admin.ModelAdmin): | |||||
""" | |||||
List display for Scheduled reports in Django admin | |||||
""" | |||||
model = ScheduledReport | |||||
list_display = ('id', 'get_recipients') | |||||
inlines = [ | |||||
ReportRecipientAdmin | |||||
] | |||||
form = ScheduledReportForm | |||||
def get_recipients(self, model): | |||||
recipients = model.reportrecep.all().values_list('email', flat=True) | |||||
if not recipients: | |||||
return 'No recipients added' | |||||
recipient_list = '' | |||||
for recipient in recipients: | |||||
recipient_list = recipient_list + recipient + ', ' | |||||
return recipient_list[:-2] | |||||
get_recipients.short_description = 'Recipients' | |||||
get_recipients.allow_tags = True | |||||
class ScheduledReportGroupAdmin(admin.ModelAdmin): | |||||
""" | |||||
List display for ScheduledReportGroup Admin | |||||
""" | |||||
model = ScheduledReportGroup | |||||
list_display = ('get_scheduled_report_name','get_report_name') | |||||
def get_scheduled_report_name(self, model): | |||||
return model.scheduled_report.subject | |||||
def get_report_name(self, model): | |||||
return model.report.name | |||||
get_scheduled_report_name.short_description = "Scheduled Report Name" | |||||
get_report_name.short_description = "Report Name" | |||||
show_change_link = True | |||||
get_report_name.allow_tags = True | |||||
admin.site.register(ScheduledReport, ScheduledReportAdmin) | |||||
admin.site.register(ScheduledReportGroup, ScheduledReportGroupAdmin) |
from __future__ import absolute_import | |||||
import os | |||||
from celery import Celery | |||||
# set the default Django settings module for the 'celery' program. | |||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') | |||||
from django.conf import settings | |||||
app = Celery('application') | |||||
# Using a string here means the worker don't have to serialize | |||||
# the configuration object to child processes. | |||||
app.config_from_object('django.conf:settings') | |||||
# Load task modules from all registered Django app configs. | |||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) |
from datetime import datetime | from datetime import datetime | ||||
from croniter import croniter | from croniter import croniter | ||||
from django.forms import ModelForm, ValidationError | from django.forms import ModelForm, ValidationError | ||||
from .models import ScheduledReport | |||||
class PostForm(forms.ModelForm): | class PostForm(forms.ModelForm): | ||||
class Meta: | class Meta: | ||||
class Meta: | class Meta: | ||||
model = CustomUser | model = CustomUser | ||||
fields = ['m_tags'] | fields = ['m_tags'] | ||||
class ScheduledReportForm(ModelForm): | |||||
class Meta: | |||||
model = ScheduledReport | |||||
fields = ['subject', 'cron_expression'] | |||||
fields = ['subject', 'cron_expression'] | |||||
help_texts = {'cron_expression': 'Scheduled time is considered in UTC'} | |||||
def clean(self): | |||||
cleaned_data = super(ScheduledReportForm, self).clean() | |||||
cron_expression = cleaned_data.get("cron_expression") | |||||
try: | |||||
iter = croniter(cron_expression, datetime.now()) | |||||
except: | |||||
raise ValidationError("Incorrect cron expression:\ | |||||
The information you must include is (in order of appearance):\ | |||||
A number (or list of numbers, or range of numbers), m, representing the minute of the hour\ | |||||
A number (or list of numbers, or range of numbers), h, representing the hour of the day\ | |||||
A number (or list of numbers, or range of numbers), dom, representing the day of the month\ | |||||
A number (or list, or range), or name (or list of names), mon, representing the month of the year\ | |||||
A number (or list, or range), or name (or list of names), dow, representing the day of the week\ | |||||
The asterisks (*) in our entry tell cron that for that unit of time, the job should be run every.\ | |||||
Eg. */5 * * * * cron for executing every 5 mins") | |||||
return cleaned_data |
# Generated by Django 2.1.2 on 2018-10-30 11:23 | |||||
from django.db import migrations | |||||
class Migration(migrations.Migration): | |||||
dependencies = [ | |||||
('application', '0001_initial'), | |||||
] | |||||
operations = [ | |||||
migrations.RemoveField( | |||||
model_name='reportrecipient', | |||||
name='scheduled_report', | |||||
), | |||||
migrations.RemoveField( | |||||
model_name='scheduledreportgroup', | |||||
name='report', | |||||
), | |||||
migrations.RemoveField( | |||||
model_name='scheduledreportgroup', | |||||
name='scheduled_report', | |||||
), | |||||
migrations.DeleteModel( | |||||
name='Report', | |||||
), | |||||
migrations.DeleteModel( | |||||
name='ReportRecipient', | |||||
), | |||||
migrations.DeleteModel( | |||||
name='ScheduledReport', | |||||
), | |||||
migrations.DeleteModel( | |||||
name='ScheduledReportGroup', | |||||
), | |||||
] |
self.save() | self.save() | ||||
def __str__(self): | def __str__(self): | ||||
return self.title | |||||
class Report(models.Model): | |||||
report_text = models.TextField() | |||||
class ScheduledReport(models.Model): | |||||
""" | |||||
Contains email subject and cron expression,to evaluate when the email has to be sent | |||||
""" | |||||
subject = models.CharField(max_length=200) | |||||
last_run_at = models.DateTimeField(null=True, blank=True) | |||||
next_run_at = models.DateTimeField(null=True, blank=True) | |||||
cron_expression = models.CharField(max_length=200) | |||||
def save(self, *args, **kwargs): | |||||
""" | |||||
function to evaluate "next_run_at" using the cron expression, so that it is updated once the report is sent. | |||||
""" | |||||
self.last_run_at = datetime.now() | |||||
iter = croniter(self.cron_expression, self.last_run_at) | |||||
self.next_run_at = iter.get_next(datetime) | |||||
super(ScheduledReport, self).save(*args, **kwargs) | |||||
def __unicode__(self): | |||||
return self.subject | |||||
class ScheduledReportGroup(models.Model): | |||||
""" | |||||
Many to many mapping between reports which will be sent out in a scheduled report | |||||
""" | |||||
report = models.ForeignKey(Report, related_name='report', on_delete=models.CASCADE) | |||||
scheduled_report = models.ForeignKey(ScheduledReport, | |||||
related_name='relatedscheduledreport', on_delete=models.CASCADE) | |||||
class ReportRecipient(models.Model): | |||||
""" | |||||
Stores all the recipients of the given scheduled report | |||||
""" | |||||
email = models.EmailField() | |||||
scheduled_report = models.ForeignKey(ScheduledReport, related_name='reportrecep', on_delete=models.CASCADE) | |||||
return self.title |
from celery.task.schedules import crontab | |||||
from celery.decorators import periodic_task | |||||
from .email_service import send_emails | |||||
# this will run every minute, see http://celeryproject.org/docs/reference/celery.task.schedules.html#celery.task.schedules.crontab | |||||
@periodic_task(run_every=crontab(hour="*", minute="*", day_of_week="*")) | |||||
def trigger_emails(): | |||||
send_emails() |
[24/Oct/2018 19:03:28] INFO [mysite:191] <QuerySet [<Post: Third one>]> | [24/Oct/2018 19:03:28] INFO [mysite:191] <QuerySet [<Post: Third one>]> | ||||
[24/Oct/2018 19:03:45] INFO [mysite:189] bamberg | [24/Oct/2018 19:03:45] INFO [mysite:189] bamberg | ||||
[24/Oct/2018 19:03:45] INFO [mysite:191] <QuerySet [<Post: Third one>, <Post: here i go again>]> | [24/Oct/2018 19:03:45] INFO [mysite:191] <QuerySet [<Post: Third one>, <Post: here i go again>]> | ||||
[30/Oct/2018 12:25:09] INFO [mysite:56] <QuerySet [<Post: Hi there>]> | |||||
[30/Oct/2018 12:25:11] INFO [mysite:56] <QuerySet [<Post: Hi there>, <Post: Bavaria>]> | |||||
[30/Oct/2018 12:25:26] INFO [mysite:189] None | |||||
[30/Oct/2018 12:25:34] INFO [mysite:189] bayern |
import os | import os | ||||
import re | import re | ||||
import socket | import socket | ||||
import djcelery | |||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) | ||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||||
'application', | 'application', | ||||
'taggit', | 'taggit', | ||||
'taggit_templatetags2', | 'taggit_templatetags2', | ||||
'djcelery', | |||||
'kombu.transport.django', | 'kombu.transport.django', | ||||
] | ] | ||||
DEBUG_TOOLBAR_CONFIG = { | DEBUG_TOOLBAR_CONFIG = { | ||||
'INTERCEPT_REDIRECTS': False, | 'INTERCEPT_REDIRECTS': False, | ||||
} | } | ||||
# Celery settings | |||||
BROKER_URL = 'django://' | |||||
CELERY_ACCEPT_CONTENT = ['json'] | |||||
CELERY_TASK_SERIALIZER = 'json' | |||||
CELERY_RESULT_SERIALIZER = 'json' | |||||
CELERY_RESULT_BACKEND = 'djcelery.backends.database:DatabaseBackend' | |||||
CELERYBEAT_SCHEDULER = "djcelery.schedulers.DatabaseScheduler" | |||||
djcelery.setup_loader() |
# -*- coding: utf-8 -*- | |||||
"""Distributed Task Queue""" | |||||
# :copyright: (c) 2015 Ask Solem and individual contributors. | |||||
# All rights # reserved. | |||||
# :copyright: (c) 2012-2014 GoPivotal, Inc., All rights reserved. | |||||
# :copyright: (c) 2009 - 2012 Ask Solem and individual contributors, | |||||
# All rights reserved. | |||||
# :license: BSD (3 Clause), see LICENSE for more details. | |||||
from __future__ import absolute_import | |||||
import os | |||||
import sys | |||||
from collections import namedtuple | |||||
version_info_t = namedtuple( | |||||
'version_info_t', ('major', 'minor', 'micro', 'releaselevel', 'serial'), | |||||
) | |||||
SERIES = 'Cipater' | |||||
VERSION = version_info_t(3, 1, 26, '.post2', '') | |||||
__version__ = '{0.major}.{0.minor}.{0.micro}{0.releaselevel}'.format(VERSION) | |||||
__author__ = 'Ask Solem' | |||||
__contact__ = 'ask@celeryproject.org' | |||||
__homepage__ = 'http://celeryproject.org' | |||||
__docformat__ = 'restructuredtext' | |||||
__all__ = [ | |||||
'Celery', 'bugreport', 'shared_task', 'task', | |||||
'current_app', 'current_task', 'maybe_signature', | |||||
'chain', 'chord', 'chunks', 'group', 'signature', | |||||
'xmap', 'xstarmap', 'uuid', 'version', '__version__', | |||||
] | |||||
VERSION_BANNER = '{0} ({1})'.format(__version__, SERIES) | |||||
# -eof meta- | |||||
if os.environ.get('C_IMPDEBUG'): # pragma: no cover | |||||
from .five import builtins | |||||
real_import = builtins.__import__ | |||||
def debug_import(name, locals=None, globals=None, | |||||
fromlist=None, level=-1): | |||||
glob = globals or getattr(sys, 'emarfteg_'[::-1])(1).f_globals | |||||
importer_name = glob and glob.get('__name__') or 'unknown' | |||||
print('-- {0} imports {1}'.format(importer_name, name)) | |||||
return real_import(name, locals, globals, fromlist, level) | |||||
builtins.__import__ = debug_import | |||||
# This is never executed, but tricks static analyzers (PyDev, PyCharm, | |||||
# pylint, etc.) into knowing the types of these symbols, and what | |||||
# they contain. | |||||
STATICA_HACK = True | |||||
globals()['kcah_acitats'[::-1].upper()] = False | |||||
if STATICA_HACK: # pragma: no cover | |||||
from celery.app import shared_task # noqa | |||||
from celery.app.base import Celery # noqa | |||||
from celery.app.utils import bugreport # noqa | |||||
from celery.app.task import Task # noqa | |||||
from celery._state import current_app, current_task # noqa | |||||
from celery.canvas import ( # noqa | |||||
chain, chord, chunks, group, | |||||
signature, maybe_signature, xmap, xstarmap, subtask, | |||||
) | |||||
from celery.utils import uuid # noqa | |||||
# Eventlet/gevent patching must happen before importing | |||||
# anything else, so these tools must be at top-level. | |||||
def _find_option_with_arg(argv, short_opts=None, long_opts=None): | |||||
"""Search argv for option specifying its short and longopt | |||||
alternatives. | |||||
Return the value of the option if found. | |||||
""" | |||||
for i, arg in enumerate(argv): | |||||
if arg.startswith('-'): | |||||
if long_opts and arg.startswith('--'): | |||||
name, _, val = arg.partition('=') | |||||
if name in long_opts: | |||||
return val | |||||
if short_opts and arg in short_opts: | |||||
return argv[i + 1] | |||||
raise KeyError('|'.join(short_opts or [] + long_opts or [])) | |||||
def _patch_eventlet(): | |||||
import eventlet | |||||
import eventlet.debug | |||||
eventlet.monkey_patch() | |||||
EVENTLET_DBLOCK = int(os.environ.get('EVENTLET_NOBLOCK', 0)) | |||||
if EVENTLET_DBLOCK: | |||||
eventlet.debug.hub_blocking_detection(EVENTLET_DBLOCK) | |||||
def _patch_gevent(): | |||||
from gevent import monkey, version_info | |||||
monkey.patch_all() | |||||
if version_info[0] == 0: # pragma: no cover | |||||
# Signals aren't working in gevent versions <1.0, | |||||
# and are not monkey patched by patch_all() | |||||
from gevent import signal as _gevent_signal | |||||
_signal = __import__('signal') | |||||
_signal.signal = _gevent_signal | |||||
def maybe_patch_concurrency(argv=sys.argv, | |||||
short_opts=['-P'], long_opts=['--pool'], | |||||
patches={'eventlet': _patch_eventlet, | |||||
'gevent': _patch_gevent}): | |||||
"""With short and long opt alternatives that specify the command line | |||||
option to set the pool, this makes sure that anything that needs | |||||
to be patched is completed as early as possible. | |||||
(e.g. eventlet/gevent monkey patches).""" | |||||
try: | |||||
pool = _find_option_with_arg(argv, short_opts, long_opts) | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
try: | |||||
patcher = patches[pool] | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
patcher() | |||||
# set up eventlet/gevent environments ASAP. | |||||
from celery import concurrency | |||||
concurrency.get_implementation(pool) | |||||
# Lazy loading | |||||
from celery import five # noqa | |||||
old_module, new_module = five.recreate_module( # pragma: no cover | |||||
__name__, | |||||
by_module={ | |||||
'celery.app': ['Celery', 'bugreport', 'shared_task'], | |||||
'celery.app.task': ['Task'], | |||||
'celery._state': ['current_app', 'current_task'], | |||||
'celery.canvas': ['chain', 'chord', 'chunks', 'group', | |||||
'signature', 'maybe_signature', 'subtask', | |||||
'xmap', 'xstarmap'], | |||||
'celery.utils': ['uuid'], | |||||
}, | |||||
direct={'task': 'celery.task'}, | |||||
__package__='celery', __file__=__file__, | |||||
__path__=__path__, __doc__=__doc__, __version__=__version__, | |||||
__author__=__author__, __contact__=__contact__, | |||||
__homepage__=__homepage__, __docformat__=__docformat__, five=five, | |||||
VERSION=VERSION, SERIES=SERIES, VERSION_BANNER=VERSION_BANNER, | |||||
version_info_t=version_info_t, | |||||
maybe_patch_concurrency=maybe_patch_concurrency, | |||||
_find_option_with_arg=_find_option_with_arg, | |||||
) |
from __future__ import absolute_import | |||||
import sys | |||||
from os.path import basename | |||||
from . import maybe_patch_concurrency | |||||
__all__ = ['main'] | |||||
DEPRECATED_FMT = """ | |||||
The {old!r} command is deprecated, please use {new!r} instead: | |||||
$ {new_argv} | |||||
""" | |||||
def _warn_deprecated(new): | |||||
print(DEPRECATED_FMT.format( | |||||
old=basename(sys.argv[0]), new=new, | |||||
new_argv=' '.join([new] + sys.argv[1:])), | |||||
) | |||||
def main(): | |||||
if 'multi' not in sys.argv: | |||||
maybe_patch_concurrency() | |||||
from celery.bin.celery import main | |||||
main() | |||||
def _compat_worker(): | |||||
maybe_patch_concurrency() | |||||
_warn_deprecated('celery worker') | |||||
from celery.bin.worker import main | |||||
main() | |||||
def _compat_multi(): | |||||
_warn_deprecated('celery multi') | |||||
from celery.bin.multi import main | |||||
main() | |||||
def _compat_beat(): | |||||
maybe_patch_concurrency() | |||||
_warn_deprecated('celery beat') | |||||
from celery.bin.beat import main | |||||
main() | |||||
if __name__ == '__main__': # pragma: no cover | |||||
main() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery._state | |||||
~~~~~~~~~~~~~~~ | |||||
This is an internal module containing thread state | |||||
like the ``current_app``, and ``current_task``. | |||||
This module shouldn't be used directly. | |||||
""" | |||||
from __future__ import absolute_import, print_function | |||||
import os | |||||
import sys | |||||
import threading | |||||
import weakref | |||||
from celery.local import Proxy | |||||
from celery.utils.threads import LocalStack | |||||
try: | |||||
from weakref import WeakSet as AppSet | |||||
except ImportError: # XXX Py2.6 | |||||
class AppSet(object): # noqa | |||||
def __init__(self): | |||||
self._refs = set() | |||||
def add(self, app): | |||||
self._refs.add(weakref.ref(app)) | |||||
def __iter__(self): | |||||
dirty = [] | |||||
try: | |||||
for appref in self._refs: | |||||
app = appref() | |||||
if app is None: | |||||
dirty.append(appref) | |||||
else: | |||||
yield app | |||||
finally: | |||||
while dirty: | |||||
self._refs.discard(dirty.pop()) | |||||
__all__ = ['set_default_app', 'get_current_app', 'get_current_task', | |||||
'get_current_worker_task', 'current_app', 'current_task', | |||||
'connect_on_app_finalize'] | |||||
#: Global default app used when no current app. | |||||
default_app = None | |||||
#: List of all app instances (weakrefs), must not be used directly. | |||||
_apps = AppSet() | |||||
#: global set of functions to call whenever a new app is finalized | |||||
#: E.g. Shared tasks, and builtin tasks are created | |||||
#: by adding callbacks here. | |||||
_on_app_finalizers = set() | |||||
_task_join_will_block = False | |||||
def connect_on_app_finalize(callback): | |||||
_on_app_finalizers.add(callback) | |||||
return callback | |||||
def _announce_app_finalized(app): | |||||
callbacks = set(_on_app_finalizers) | |||||
for callback in callbacks: | |||||
callback(app) | |||||
def _set_task_join_will_block(blocks): | |||||
global _task_join_will_block | |||||
_task_join_will_block = blocks | |||||
def task_join_will_block(): | |||||
return _task_join_will_block | |||||
class _TLS(threading.local): | |||||
#: Apps with the :attr:`~celery.app.base.BaseApp.set_as_current` attribute | |||||
#: sets this, so it will always contain the last instantiated app, | |||||
#: and is the default app returned by :func:`app_or_default`. | |||||
current_app = None | |||||
_tls = _TLS() | |||||
_task_stack = LocalStack() | |||||
def set_default_app(app): | |||||
global default_app | |||||
default_app = app | |||||
def _get_current_app(): | |||||
if default_app is None: | |||||
#: creates the global fallback app instance. | |||||
from celery.app import Celery | |||||
set_default_app(Celery( | |||||
'default', | |||||
loader=os.environ.get('CELERY_LOADER') or 'default', | |||||
fixups=[], | |||||
set_as_current=False, accept_magic_kwargs=True, | |||||
)) | |||||
return _tls.current_app or default_app | |||||
def _set_current_app(app): | |||||
_tls.current_app = app | |||||
C_STRICT_APP = os.environ.get('C_STRICT_APP') | |||||
if os.environ.get('C_STRICT_APP'): # pragma: no cover | |||||
def get_current_app(): | |||||
raise Exception('USES CURRENT APP') | |||||
import traceback | |||||
print('-- USES CURRENT_APP', file=sys.stderr) # noqa+ | |||||
traceback.print_stack(file=sys.stderr) | |||||
return _get_current_app() | |||||
else: | |||||
get_current_app = _get_current_app | |||||
def get_current_task(): | |||||
"""Currently executing task.""" | |||||
return _task_stack.top | |||||
def get_current_worker_task(): | |||||
"""Currently executing task, that was applied by the worker. | |||||
This is used to differentiate between the actual task | |||||
executed by the worker and any task that was called within | |||||
a task (using ``task.__call__`` or ``task.apply``) | |||||
""" | |||||
for task in reversed(_task_stack.stack): | |||||
if not task.request.called_directly: | |||||
return task | |||||
#: Proxy to current app. | |||||
current_app = Proxy(get_current_app) | |||||
#: Proxy to current task. | |||||
current_task = Proxy(get_current_task) | |||||
def _register_app(app): | |||||
_apps.add(app) | |||||
def _get_active_apps(): | |||||
return _apps |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app | |||||
~~~~~~~~~~ | |||||
Celery Application. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import os | |||||
from celery.local import Proxy | |||||
from celery import _state | |||||
from celery._state import ( | |||||
get_current_app as current_app, | |||||
get_current_task as current_task, | |||||
connect_on_app_finalize, set_default_app, _get_active_apps, _task_stack, | |||||
) | |||||
from celery.utils import gen_task_name | |||||
from .base import Celery, AppPickler | |||||
__all__ = ['Celery', 'AppPickler', 'default_app', 'app_or_default', | |||||
'bugreport', 'enable_trace', 'disable_trace', 'shared_task', | |||||
'set_default_app', 'current_app', 'current_task', | |||||
'push_current_task', 'pop_current_task'] | |||||
#: Proxy always returning the app set as default. | |||||
default_app = Proxy(lambda: _state.default_app) | |||||
#: Function returning the app provided or the default app if none. | |||||
#: | |||||
#: The environment variable :envvar:`CELERY_TRACE_APP` is used to | |||||
#: trace app leaks. When enabled an exception is raised if there | |||||
#: is no active app. | |||||
app_or_default = None | |||||
#: The 'default' loader is the default loader used by old applications. | |||||
#: This is deprecated and should no longer be used as it's set too early | |||||
#: to be affected by --loader argument. | |||||
default_loader = os.environ.get('CELERY_LOADER') or 'default' # XXX | |||||
#: Function used to push a task to the thread local stack | |||||
#: keeping track of the currently executing task. | |||||
#: You must remember to pop the task after. | |||||
push_current_task = _task_stack.push | |||||
#: Function used to pop a task from the thread local stack | |||||
#: keeping track of the currently executing task. | |||||
pop_current_task = _task_stack.pop | |||||
def bugreport(app=None): | |||||
return (app or current_app()).bugreport() | |||||
def _app_or_default(app=None): | |||||
if app is None: | |||||
return _state.get_current_app() | |||||
return app | |||||
def _app_or_default_trace(app=None): # pragma: no cover | |||||
from traceback import print_stack | |||||
from billiard import current_process | |||||
if app is None: | |||||
if getattr(_state._tls, 'current_app', None): | |||||
print('-- RETURNING TO CURRENT APP --') # noqa+ | |||||
print_stack() | |||||
return _state._tls.current_app | |||||
if current_process()._name == 'MainProcess': | |||||
raise Exception('DEFAULT APP') | |||||
print('-- RETURNING TO DEFAULT APP --') # noqa+ | |||||
print_stack() | |||||
return _state.default_app | |||||
return app | |||||
def enable_trace(): | |||||
global app_or_default | |||||
app_or_default = _app_or_default_trace | |||||
def disable_trace(): | |||||
global app_or_default | |||||
app_or_default = _app_or_default | |||||
if os.environ.get('CELERY_TRACE_APP'): # pragma: no cover | |||||
enable_trace() | |||||
else: | |||||
disable_trace() | |||||
App = Celery # XXX Compat | |||||
def shared_task(*args, **kwargs): | |||||
"""Create shared tasks (decorator). | |||||
Will return a proxy that always takes the task from the current apps | |||||
task registry. | |||||
This can be used by library authors to create tasks that will work | |||||
for any app environment. | |||||
Example: | |||||
>>> from celery import Celery, shared_task | |||||
>>> @shared_task | |||||
... def add(x, y): | |||||
... return x + y | |||||
>>> app1 = Celery(broker='amqp://') | |||||
>>> add.app is app1 | |||||
True | |||||
>>> app2 = Celery(broker='redis://') | |||||
>>> add.app is app2 | |||||
""" | |||||
def create_shared_task(**options): | |||||
def __inner(fun): | |||||
name = options.get('name') | |||||
# Set as shared task so that unfinalized apps, | |||||
# and future apps will load the task. | |||||
connect_on_app_finalize( | |||||
lambda app: app._task_from_fun(fun, **options) | |||||
) | |||||
# Force all finalized apps to take this task as well. | |||||
for app in _get_active_apps(): | |||||
if app.finalized: | |||||
with app._finalize_mutex: | |||||
app._task_from_fun(fun, **options) | |||||
# Return a proxy that always gets the task from the current | |||||
# apps task registry. | |||||
def task_by_cons(): | |||||
app = current_app() | |||||
return app.tasks[ | |||||
name or gen_task_name(app, fun.__name__, fun.__module__) | |||||
] | |||||
return Proxy(task_by_cons) | |||||
return __inner | |||||
if len(args) == 1 and callable(args[0]): | |||||
return create_shared_task(**kwargs)(args[0]) | |||||
return create_shared_task(*args, **kwargs) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.amqp | |||||
~~~~~~~~~~~~~~~ | |||||
Sending and receiving messages using Kombu. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import numbers | |||||
from datetime import timedelta | |||||
from weakref import WeakValueDictionary | |||||
from kombu import Connection, Consumer, Exchange, Producer, Queue | |||||
from kombu.common import Broadcast | |||||
from kombu.pools import ProducerPool | |||||
from kombu.utils import cached_property, uuid | |||||
from kombu.utils.encoding import safe_repr | |||||
from kombu.utils.functional import maybe_list | |||||
from celery import signals | |||||
from celery.five import items, string_t | |||||
from celery.utils.text import indent as textindent | |||||
from celery.utils.timeutils import to_utc | |||||
from . import app_or_default | |||||
from . import routes as _routes | |||||
__all__ = ['AMQP', 'Queues', 'TaskProducer', 'TaskConsumer'] | |||||
#: earliest date supported by time.mktime. | |||||
INT_MIN = -2147483648 | |||||
#: Human readable queue declaration. | |||||
QUEUE_FORMAT = """ | |||||
.> {0.name:<16} exchange={0.exchange.name}({0.exchange.type}) \ | |||||
key={0.routing_key} | |||||
""" | |||||
class Queues(dict): | |||||
"""Queue name⇒ declaration mapping. | |||||
:param queues: Initial list/tuple or dict of queues. | |||||
:keyword create_missing: By default any unknown queues will be | |||||
added automatically, but if disabled | |||||
the occurrence of unknown queues | |||||
in `wanted` will raise :exc:`KeyError`. | |||||
:keyword ha_policy: Default HA policy for queues with none set. | |||||
""" | |||||
#: If set, this is a subset of queues to consume from. | |||||
#: The rest of the queues are then used for routing only. | |||||
_consume_from = None | |||||
def __init__(self, queues=None, default_exchange=None, | |||||
create_missing=True, ha_policy=None, autoexchange=None): | |||||
dict.__init__(self) | |||||
self.aliases = WeakValueDictionary() | |||||
self.default_exchange = default_exchange | |||||
self.create_missing = create_missing | |||||
self.ha_policy = ha_policy | |||||
self.autoexchange = Exchange if autoexchange is None else autoexchange | |||||
if isinstance(queues, (tuple, list)): | |||||
queues = dict((q.name, q) for q in queues) | |||||
for name, q in items(queues or {}): | |||||
self.add(q) if isinstance(q, Queue) else self.add_compat(name, **q) | |||||
def __getitem__(self, name): | |||||
try: | |||||
return self.aliases[name] | |||||
except KeyError: | |||||
return dict.__getitem__(self, name) | |||||
def __setitem__(self, name, queue): | |||||
if self.default_exchange and (not queue.exchange or | |||||
not queue.exchange.name): | |||||
queue.exchange = self.default_exchange | |||||
dict.__setitem__(self, name, queue) | |||||
if queue.alias: | |||||
self.aliases[queue.alias] = queue | |||||
def __missing__(self, name): | |||||
if self.create_missing: | |||||
return self.add(self.new_missing(name)) | |||||
raise KeyError(name) | |||||
def add(self, queue, **kwargs): | |||||
"""Add new queue. | |||||
The first argument can either be a :class:`kombu.Queue` instance, | |||||
or the name of a queue. If the former the rest of the keyword | |||||
arguments are ignored, and options are simply taken from the queue | |||||
instance. | |||||
:param queue: :class:`kombu.Queue` instance or name of the queue. | |||||
:keyword exchange: (if named) specifies exchange name. | |||||
:keyword routing_key: (if named) specifies binding key. | |||||
:keyword exchange_type: (if named) specifies type of exchange. | |||||
:keyword \*\*options: (if named) Additional declaration options. | |||||
""" | |||||
if not isinstance(queue, Queue): | |||||
return self.add_compat(queue, **kwargs) | |||||
if self.ha_policy: | |||||
if queue.queue_arguments is None: | |||||
queue.queue_arguments = {} | |||||
self._set_ha_policy(queue.queue_arguments) | |||||
self[queue.name] = queue | |||||
return queue | |||||
def add_compat(self, name, **options): | |||||
# docs used to use binding_key as routing key | |||||
options.setdefault('routing_key', options.get('binding_key')) | |||||
if options['routing_key'] is None: | |||||
options['routing_key'] = name | |||||
if self.ha_policy is not None: | |||||
self._set_ha_policy(options.setdefault('queue_arguments', {})) | |||||
q = self[name] = Queue.from_dict(name, **options) | |||||
return q | |||||
def _set_ha_policy(self, args): | |||||
policy = self.ha_policy | |||||
if isinstance(policy, (list, tuple)): | |||||
return args.update({'x-ha-policy': 'nodes', | |||||
'x-ha-policy-params': list(policy)}) | |||||
args['x-ha-policy'] = policy | |||||
def format(self, indent=0, indent_first=True): | |||||
"""Format routing table into string for log dumps.""" | |||||
active = self.consume_from | |||||
if not active: | |||||
return '' | |||||
info = [QUEUE_FORMAT.strip().format(q) | |||||
for _, q in sorted(items(active))] | |||||
if indent_first: | |||||
return textindent('\n'.join(info), indent) | |||||
return info[0] + '\n' + textindent('\n'.join(info[1:]), indent) | |||||
def select_add(self, queue, **kwargs): | |||||
"""Add new task queue that will be consumed from even when | |||||
a subset has been selected using the :option:`-Q` option.""" | |||||
q = self.add(queue, **kwargs) | |||||
if self._consume_from is not None: | |||||
self._consume_from[q.name] = q | |||||
return q | |||||
def select(self, include): | |||||
"""Sets :attr:`consume_from` by selecting a subset of the | |||||
currently defined queues. | |||||
:param include: Names of queues to consume from. | |||||
Can be iterable or string. | |||||
""" | |||||
if include: | |||||
self._consume_from = dict((name, self[name]) | |||||
for name in maybe_list(include)) | |||||
select_subset = select # XXX compat | |||||
def deselect(self, exclude): | |||||
"""Deselect queues so that they will not be consumed from. | |||||
:param exclude: Names of queues to avoid consuming from. | |||||
Can be iterable or string. | |||||
""" | |||||
if exclude: | |||||
exclude = maybe_list(exclude) | |||||
if self._consume_from is None: | |||||
# using selection | |||||
return self.select(k for k in self if k not in exclude) | |||||
# using all queues | |||||
for queue in exclude: | |||||
self._consume_from.pop(queue, None) | |||||
select_remove = deselect # XXX compat | |||||
def new_missing(self, name): | |||||
return Queue(name, self.autoexchange(name), name) | |||||
@property | |||||
def consume_from(self): | |||||
if self._consume_from is not None: | |||||
return self._consume_from | |||||
return self | |||||
class TaskProducer(Producer): | |||||
app = None | |||||
auto_declare = False | |||||
retry = False | |||||
retry_policy = None | |||||
utc = True | |||||
event_dispatcher = None | |||||
send_sent_event = False | |||||
def __init__(self, channel=None, exchange=None, *args, **kwargs): | |||||
self.retry = kwargs.pop('retry', self.retry) | |||||
self.retry_policy = kwargs.pop('retry_policy', | |||||
self.retry_policy or {}) | |||||
self.send_sent_event = kwargs.pop('send_sent_event', | |||||
self.send_sent_event) | |||||
exchange = exchange or self.exchange | |||||
self.queues = self.app.amqp.queues # shortcut | |||||
self.default_queue = self.app.amqp.default_queue | |||||
self._default_mode = self.app.conf.CELERY_DEFAULT_DELIVERY_MODE | |||||
super(TaskProducer, self).__init__(channel, exchange, *args, **kwargs) | |||||
def publish_task(self, task_name, task_args=None, task_kwargs=None, | |||||
countdown=None, eta=None, task_id=None, group_id=None, | |||||
taskset_id=None, # compat alias to group_id | |||||
expires=None, exchange=None, exchange_type=None, | |||||
event_dispatcher=None, retry=None, retry_policy=None, | |||||
queue=None, now=None, retries=0, chord=None, | |||||
callbacks=None, errbacks=None, routing_key=None, | |||||
serializer=None, delivery_mode=None, compression=None, | |||||
reply_to=None, time_limit=None, soft_time_limit=None, | |||||
declare=None, headers=None, | |||||
send_before_publish=signals.before_task_publish.send, | |||||
before_receivers=signals.before_task_publish.receivers, | |||||
send_after_publish=signals.after_task_publish.send, | |||||
after_receivers=signals.after_task_publish.receivers, | |||||
send_task_sent=signals.task_sent.send, # XXX deprecated | |||||
sent_receivers=signals.task_sent.receivers, | |||||
**kwargs): | |||||
"""Send task message.""" | |||||
retry = self.retry if retry is None else retry | |||||
headers = {} if headers is None else headers | |||||
qname = queue | |||||
if queue is None and exchange is None: | |||||
queue = self.default_queue | |||||
if queue is not None: | |||||
if isinstance(queue, string_t): | |||||
qname, queue = queue, self.queues[queue] | |||||
else: | |||||
qname = queue.name | |||||
exchange = exchange or queue.exchange.name | |||||
routing_key = routing_key or queue.routing_key | |||||
if declare is None and queue and not isinstance(queue, Broadcast): | |||||
declare = [queue] | |||||
if delivery_mode is None: | |||||
delivery_mode = self._default_mode | |||||
# merge default and custom policy | |||||
retry = self.retry if retry is None else retry | |||||
_rp = (dict(self.retry_policy, **retry_policy) if retry_policy | |||||
else self.retry_policy) | |||||
task_id = task_id or uuid() | |||||
task_args = task_args or [] | |||||
task_kwargs = task_kwargs or {} | |||||
if not isinstance(task_args, (list, tuple)): | |||||
raise ValueError('task args must be a list or tuple') | |||||
if not isinstance(task_kwargs, dict): | |||||
raise ValueError('task kwargs must be a dictionary') | |||||
if countdown: # Convert countdown to ETA. | |||||
self._verify_seconds(countdown, 'countdown') | |||||
now = now or self.app.now() | |||||
eta = now + timedelta(seconds=countdown) | |||||
if self.utc: | |||||
eta = to_utc(eta).astimezone(self.app.timezone) | |||||
if isinstance(expires, numbers.Real): | |||||
self._verify_seconds(expires, 'expires') | |||||
now = now or self.app.now() | |||||
expires = now + timedelta(seconds=expires) | |||||
if self.utc: | |||||
expires = to_utc(expires).astimezone(self.app.timezone) | |||||
eta = eta and eta.isoformat() | |||||
expires = expires and expires.isoformat() | |||||
body = { | |||||
'task': task_name, | |||||
'id': task_id, | |||||
'args': task_args, | |||||
'kwargs': task_kwargs, | |||||
'retries': retries or 0, | |||||
'eta': eta, | |||||
'expires': expires, | |||||
'utc': self.utc, | |||||
'callbacks': callbacks, | |||||
'errbacks': errbacks, | |||||
'timelimit': (time_limit, soft_time_limit), | |||||
'taskset': group_id or taskset_id, | |||||
'chord': chord, | |||||
} | |||||
if before_receivers: | |||||
send_before_publish( | |||||
sender=task_name, body=body, | |||||
exchange=exchange, | |||||
routing_key=routing_key, | |||||
declare=declare, | |||||
headers=headers, | |||||
properties=kwargs, | |||||
retry_policy=retry_policy, | |||||
) | |||||
self.publish( | |||||
body, | |||||
exchange=exchange, routing_key=routing_key, | |||||
serializer=serializer or self.serializer, | |||||
compression=compression or self.compression, | |||||
headers=headers, | |||||
retry=retry, retry_policy=_rp, | |||||
reply_to=reply_to, | |||||
correlation_id=task_id, | |||||
delivery_mode=delivery_mode, declare=declare, | |||||
**kwargs | |||||
) | |||||
if after_receivers: | |||||
send_after_publish(sender=task_name, body=body, | |||||
exchange=exchange, routing_key=routing_key) | |||||
if sent_receivers: # XXX deprecated | |||||
send_task_sent(sender=task_name, task_id=task_id, | |||||
task=task_name, args=task_args, | |||||
kwargs=task_kwargs, eta=eta, | |||||
taskset=group_id or taskset_id) | |||||
if self.send_sent_event: | |||||
evd = event_dispatcher or self.event_dispatcher | |||||
exname = exchange or self.exchange | |||||
if isinstance(exname, Exchange): | |||||
exname = exname.name | |||||
evd.publish( | |||||
'task-sent', | |||||
{ | |||||
'uuid': task_id, | |||||
'name': task_name, | |||||
'args': safe_repr(task_args), | |||||
'kwargs': safe_repr(task_kwargs), | |||||
'retries': retries, | |||||
'eta': eta, | |||||
'expires': expires, | |||||
'queue': qname, | |||||
'exchange': exname, | |||||
'routing_key': routing_key, | |||||
}, | |||||
self, retry=retry, retry_policy=retry_policy, | |||||
) | |||||
return task_id | |||||
delay_task = publish_task # XXX Compat | |||||
def _verify_seconds(self, s, what): | |||||
if s < INT_MIN: | |||||
raise ValueError('%s is out of range: %r' % (what, s)) | |||||
return s | |||||
@cached_property | |||||
def event_dispatcher(self): | |||||
# We call Dispatcher.publish with a custom producer | |||||
# so don't need the dispatcher to be "enabled". | |||||
return self.app.events.Dispatcher(enabled=False) | |||||
class TaskPublisher(TaskProducer): | |||||
"""Deprecated version of :class:`TaskProducer`.""" | |||||
def __init__(self, channel=None, exchange=None, *args, **kwargs): | |||||
self.app = app_or_default(kwargs.pop('app', self.app)) | |||||
self.retry = kwargs.pop('retry', self.retry) | |||||
self.retry_policy = kwargs.pop('retry_policy', | |||||
self.retry_policy or {}) | |||||
exchange = exchange or self.exchange | |||||
if not isinstance(exchange, Exchange): | |||||
exchange = Exchange(exchange, | |||||
kwargs.pop('exchange_type', 'direct')) | |||||
self.queues = self.app.amqp.queues # shortcut | |||||
super(TaskPublisher, self).__init__(channel, exchange, *args, **kwargs) | |||||
class TaskConsumer(Consumer): | |||||
app = None | |||||
def __init__(self, channel, queues=None, app=None, accept=None, **kw): | |||||
self.app = app or self.app | |||||
if accept is None: | |||||
accept = self.app.conf.CELERY_ACCEPT_CONTENT | |||||
super(TaskConsumer, self).__init__( | |||||
channel, | |||||
queues or list(self.app.amqp.queues.consume_from.values()), | |||||
accept=accept, | |||||
**kw | |||||
) | |||||
class AMQP(object): | |||||
Connection = Connection | |||||
Consumer = Consumer | |||||
#: compat alias to Connection | |||||
BrokerConnection = Connection | |||||
producer_cls = TaskProducer | |||||
consumer_cls = TaskConsumer | |||||
queues_cls = Queues | |||||
#: Cached and prepared routing table. | |||||
_rtable = None | |||||
#: Underlying producer pool instance automatically | |||||
#: set by the :attr:`producer_pool`. | |||||
_producer_pool = None | |||||
# Exchange class/function used when defining automatic queues. | |||||
# E.g. you can use ``autoexchange = lambda n: None`` to use the | |||||
# amqp default exchange, which is a shortcut to bypass routing | |||||
# and instead send directly to the queue named in the routing key. | |||||
autoexchange = None | |||||
def __init__(self, app): | |||||
self.app = app | |||||
def flush_routes(self): | |||||
self._rtable = _routes.prepare(self.app.conf.CELERY_ROUTES) | |||||
def Queues(self, queues, create_missing=None, ha_policy=None, | |||||
autoexchange=None): | |||||
"""Create new :class:`Queues` instance, using queue defaults | |||||
from the current configuration.""" | |||||
conf = self.app.conf | |||||
if create_missing is None: | |||||
create_missing = conf.CELERY_CREATE_MISSING_QUEUES | |||||
if ha_policy is None: | |||||
ha_policy = conf.CELERY_QUEUE_HA_POLICY | |||||
if not queues and conf.CELERY_DEFAULT_QUEUE: | |||||
queues = (Queue(conf.CELERY_DEFAULT_QUEUE, | |||||
exchange=self.default_exchange, | |||||
routing_key=conf.CELERY_DEFAULT_ROUTING_KEY), ) | |||||
autoexchange = (self.autoexchange if autoexchange is None | |||||
else autoexchange) | |||||
return self.queues_cls( | |||||
queues, self.default_exchange, create_missing, | |||||
ha_policy, autoexchange, | |||||
) | |||||
def Router(self, queues=None, create_missing=None): | |||||
"""Return the current task router.""" | |||||
return _routes.Router(self.routes, queues or self.queues, | |||||
self.app.either('CELERY_CREATE_MISSING_QUEUES', | |||||
create_missing), app=self.app) | |||||
@cached_property | |||||
def TaskConsumer(self): | |||||
"""Return consumer configured to consume from the queues | |||||
we are configured for (``app.amqp.queues.consume_from``).""" | |||||
return self.app.subclass_with_self(self.consumer_cls, | |||||
reverse='amqp.TaskConsumer') | |||||
get_task_consumer = TaskConsumer # XXX compat | |||||
@cached_property | |||||
def TaskProducer(self): | |||||
"""Return publisher used to send tasks. | |||||
You should use `app.send_task` instead. | |||||
""" | |||||
conf = self.app.conf | |||||
return self.app.subclass_with_self( | |||||
self.producer_cls, | |||||
reverse='amqp.TaskProducer', | |||||
exchange=self.default_exchange, | |||||
routing_key=conf.CELERY_DEFAULT_ROUTING_KEY, | |||||
serializer=conf.CELERY_TASK_SERIALIZER, | |||||
compression=conf.CELERY_MESSAGE_COMPRESSION, | |||||
retry=conf.CELERY_TASK_PUBLISH_RETRY, | |||||
retry_policy=conf.CELERY_TASK_PUBLISH_RETRY_POLICY, | |||||
send_sent_event=conf.CELERY_SEND_TASK_SENT_EVENT, | |||||
utc=conf.CELERY_ENABLE_UTC, | |||||
) | |||||
TaskPublisher = TaskProducer # compat | |||||
@cached_property | |||||
def default_queue(self): | |||||
return self.queues[self.app.conf.CELERY_DEFAULT_QUEUE] | |||||
@cached_property | |||||
def queues(self): | |||||
"""Queue name⇒ declaration mapping.""" | |||||
return self.Queues(self.app.conf.CELERY_QUEUES) | |||||
@queues.setter # noqa | |||||
def queues(self, queues): | |||||
return self.Queues(queues) | |||||
@property | |||||
def routes(self): | |||||
if self._rtable is None: | |||||
self.flush_routes() | |||||
return self._rtable | |||||
@cached_property | |||||
def router(self): | |||||
return self.Router() | |||||
@property | |||||
def producer_pool(self): | |||||
if self._producer_pool is None: | |||||
self._producer_pool = ProducerPool( | |||||
self.app.pool, | |||||
limit=self.app.pool.limit, | |||||
Producer=self.TaskProducer, | |||||
) | |||||
return self._producer_pool | |||||
publisher_pool = producer_pool # compat alias | |||||
@cached_property | |||||
def default_exchange(self): | |||||
return Exchange(self.app.conf.CELERY_DEFAULT_EXCHANGE, | |||||
self.app.conf.CELERY_DEFAULT_EXCHANGE_TYPE) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.annotations | |||||
~~~~~~~~~~~~~~~~~~~~~~ | |||||
Annotations is a nice term for monkey patching | |||||
task classes in the configuration. | |||||
This prepares and performs the annotations in the | |||||
:setting:`CELERY_ANNOTATIONS` setting. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from celery.five import string_t | |||||
from celery.utils.functional import firstmethod, mlazy | |||||
from celery.utils.imports import instantiate | |||||
_first_match = firstmethod('annotate') | |||||
_first_match_any = firstmethod('annotate_any') | |||||
__all__ = ['MapAnnotation', 'prepare', 'resolve_all'] | |||||
class MapAnnotation(dict): | |||||
def annotate_any(self): | |||||
try: | |||||
return dict(self['*']) | |||||
except KeyError: | |||||
pass | |||||
def annotate(self, task): | |||||
try: | |||||
return dict(self[task.name]) | |||||
except KeyError: | |||||
pass | |||||
def prepare(annotations): | |||||
"""Expands the :setting:`CELERY_ANNOTATIONS` setting.""" | |||||
def expand_annotation(annotation): | |||||
if isinstance(annotation, dict): | |||||
return MapAnnotation(annotation) | |||||
elif isinstance(annotation, string_t): | |||||
return mlazy(instantiate, annotation) | |||||
return annotation | |||||
if annotations is None: | |||||
return () | |||||
elif not isinstance(annotations, (list, tuple)): | |||||
annotations = (annotations, ) | |||||
return [expand_annotation(anno) for anno in annotations] | |||||
def resolve_all(anno, task): | |||||
return (x for x in (_first_match(anno, task), _first_match_any(anno)) if x) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.base | |||||
~~~~~~~~~~~~~~~ | |||||
Actual App instance implementation. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import os | |||||
import threading | |||||
import warnings | |||||
from collections import defaultdict, deque | |||||
from copy import deepcopy | |||||
from operator import attrgetter | |||||
from amqp import promise | |||||
from billiard.util import register_after_fork | |||||
from kombu.clocks import LamportClock | |||||
from kombu.common import oid_from | |||||
from kombu.utils import cached_property, uuid | |||||
from celery import platforms | |||||
from celery import signals | |||||
from celery._state import ( | |||||
_task_stack, get_current_app, _set_current_app, set_default_app, | |||||
_register_app, get_current_worker_task, connect_on_app_finalize, | |||||
_announce_app_finalized, | |||||
) | |||||
from celery.exceptions import AlwaysEagerIgnored, ImproperlyConfigured | |||||
from celery.five import values | |||||
from celery.loaders import get_loader_cls | |||||
from celery.local import PromiseProxy, maybe_evaluate | |||||
from celery.utils.functional import first, maybe_list | |||||
from celery.utils.imports import instantiate, symbol_by_name | |||||
from celery.utils.objects import FallbackContext, mro_lookup | |||||
from .annotations import prepare as prepare_annotations | |||||
from .defaults import DEFAULTS, find_deprecated_settings | |||||
from .registry import TaskRegistry | |||||
from .utils import ( | |||||
AppPickler, Settings, bugreport, _unpickle_app, _unpickle_app_v2, appstr, | |||||
) | |||||
# Load all builtin tasks | |||||
from . import builtins # noqa | |||||
__all__ = ['Celery'] | |||||
_EXECV = os.environ.get('FORKED_BY_MULTIPROCESSING') | |||||
BUILTIN_FIXUPS = frozenset([ | |||||
'celery.fixups.django:fixup', | |||||
]) | |||||
ERR_ENVVAR_NOT_SET = """\ | |||||
The environment variable {0!r} is not set, | |||||
and as such the configuration could not be loaded. | |||||
Please set this variable and make it point to | |||||
a configuration module.""" | |||||
_after_fork_registered = False | |||||
def app_has_custom(app, attr): | |||||
return mro_lookup(app.__class__, attr, stop=(Celery, object), | |||||
monkey_patched=[__name__]) | |||||
def _unpickle_appattr(reverse_name, args): | |||||
"""Given an attribute name and a list of args, gets | |||||
the attribute from the current app and calls it.""" | |||||
return get_current_app()._rgetattr(reverse_name)(*args) | |||||
def _global_after_fork(obj): | |||||
# Previously every app would call: | |||||
# `register_after_fork(app, app._after_fork)` | |||||
# but this created a leak as `register_after_fork` stores concrete object | |||||
# references and once registered an object cannot be removed without | |||||
# touching and iterating over the private afterfork registry list. | |||||
# | |||||
# See Issue #1949 | |||||
from celery import _state | |||||
from multiprocessing import util as mputil | |||||
for app in _state._apps: | |||||
try: | |||||
app._after_fork(obj) | |||||
except Exception as exc: | |||||
if mputil._logger: | |||||
mputil._logger.info( | |||||
'after forker raised exception: %r', exc, exc_info=1) | |||||
def _ensure_after_fork(): | |||||
global _after_fork_registered | |||||
_after_fork_registered = True | |||||
register_after_fork(_global_after_fork, _global_after_fork) | |||||
class Celery(object): | |||||
#: This is deprecated, use :meth:`reduce_keys` instead | |||||
Pickler = AppPickler | |||||
SYSTEM = platforms.SYSTEM | |||||
IS_OSX, IS_WINDOWS = platforms.IS_OSX, platforms.IS_WINDOWS | |||||
amqp_cls = 'celery.app.amqp:AMQP' | |||||
backend_cls = None | |||||
events_cls = 'celery.events:Events' | |||||
loader_cls = 'celery.loaders.app:AppLoader' | |||||
log_cls = 'celery.app.log:Logging' | |||||
control_cls = 'celery.app.control:Control' | |||||
task_cls = 'celery.app.task:Task' | |||||
registry_cls = TaskRegistry | |||||
_fixups = None | |||||
_pool = None | |||||
builtin_fixups = BUILTIN_FIXUPS | |||||
def __init__(self, main=None, loader=None, backend=None, | |||||
amqp=None, events=None, log=None, control=None, | |||||
set_as_current=True, accept_magic_kwargs=False, | |||||
tasks=None, broker=None, include=None, changes=None, | |||||
config_source=None, fixups=None, task_cls=None, | |||||
autofinalize=True, **kwargs): | |||||
self.clock = LamportClock() | |||||
self.main = main | |||||
self.amqp_cls = amqp or self.amqp_cls | |||||
self.events_cls = events or self.events_cls | |||||
self.loader_cls = loader or self.loader_cls | |||||
self.log_cls = log or self.log_cls | |||||
self.control_cls = control or self.control_cls | |||||
self.task_cls = task_cls or self.task_cls | |||||
self.set_as_current = set_as_current | |||||
self.registry_cls = symbol_by_name(self.registry_cls) | |||||
self.accept_magic_kwargs = accept_magic_kwargs | |||||
self.user_options = defaultdict(set) | |||||
self.steps = defaultdict(set) | |||||
self.autofinalize = autofinalize | |||||
self.configured = False | |||||
self._config_source = config_source | |||||
self._pending_defaults = deque() | |||||
self.finalized = False | |||||
self._finalize_mutex = threading.Lock() | |||||
self._pending = deque() | |||||
self._tasks = tasks | |||||
if not isinstance(self._tasks, TaskRegistry): | |||||
self._tasks = TaskRegistry(self._tasks or {}) | |||||
# If the class defines a custom __reduce_args__ we need to use | |||||
# the old way of pickling apps, which is pickling a list of | |||||
# args instead of the new way that pickles a dict of keywords. | |||||
self._using_v1_reduce = app_has_custom(self, '__reduce_args__') | |||||
# these options are moved to the config to | |||||
# simplify pickling of the app object. | |||||
self._preconf = changes or {} | |||||
if broker: | |||||
self._preconf['BROKER_URL'] = broker | |||||
if backend: | |||||
self._preconf['CELERY_RESULT_BACKEND'] = backend | |||||
if include: | |||||
self._preconf['CELERY_IMPORTS'] = include | |||||
# - Apply fixups. | |||||
self.fixups = set(self.builtin_fixups) if fixups is None else fixups | |||||
# ...store fixup instances in _fixups to keep weakrefs alive. | |||||
self._fixups = [symbol_by_name(fixup)(self) for fixup in self.fixups] | |||||
if self.set_as_current: | |||||
self.set_current() | |||||
self.on_init() | |||||
_register_app(self) | |||||
def set_current(self): | |||||
_set_current_app(self) | |||||
def set_default(self): | |||||
set_default_app(self) | |||||
def __enter__(self): | |||||
return self | |||||
def __exit__(self, *exc_info): | |||||
self.close() | |||||
def close(self): | |||||
self._maybe_close_pool() | |||||
def on_init(self): | |||||
"""Optional callback called at init.""" | |||||
pass | |||||
def start(self, argv=None): | |||||
return instantiate( | |||||
'celery.bin.celery:CeleryCommand', | |||||
app=self).execute_from_commandline(argv) | |||||
def worker_main(self, argv=None): | |||||
return instantiate( | |||||
'celery.bin.worker:worker', | |||||
app=self).execute_from_commandline(argv) | |||||
def task(self, *args, **opts): | |||||
"""Creates new task class from any callable.""" | |||||
if _EXECV and not opts.get('_force_evaluate'): | |||||
# When using execv the task in the original module will point to a | |||||
# different app, so doing things like 'add.request' will point to | |||||
# a differnt task instance. This makes sure it will always use | |||||
# the task instance from the current app. | |||||
# Really need a better solution for this :( | |||||
from . import shared_task | |||||
return shared_task(*args, _force_evaluate=True, **opts) | |||||
def inner_create_task_cls(shared=True, filter=None, **opts): | |||||
_filt = filter # stupid 2to3 | |||||
def _create_task_cls(fun): | |||||
if shared: | |||||
def cons(app): | |||||
return app._task_from_fun(fun, **opts) | |||||
cons.__name__ = fun.__name__ | |||||
connect_on_app_finalize(cons) | |||||
if self.accept_magic_kwargs: # compat mode | |||||
task = self._task_from_fun(fun, **opts) | |||||
if filter: | |||||
task = filter(task) | |||||
return task | |||||
if self.finalized or opts.get('_force_evaluate'): | |||||
ret = self._task_from_fun(fun, **opts) | |||||
else: | |||||
# return a proxy object that evaluates on first use | |||||
ret = PromiseProxy(self._task_from_fun, (fun, ), opts, | |||||
__doc__=fun.__doc__) | |||||
self._pending.append(ret) | |||||
if _filt: | |||||
return _filt(ret) | |||||
return ret | |||||
return _create_task_cls | |||||
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) | |||||
def _task_from_fun(self, fun, **options): | |||||
if not self.finalized and not self.autofinalize: | |||||
raise RuntimeError('Contract breach: app not finalized') | |||||
base = options.pop('base', None) or self.Task | |||||
bind = options.pop('bind', False) | |||||
T = type(fun.__name__, (base, ), dict({ | |||||
'app': self, | |||||
'accept_magic_kwargs': False, | |||||
'run': fun if bind else staticmethod(fun), | |||||
'_decorated': True, | |||||
'__doc__': fun.__doc__, | |||||
'__module__': fun.__module__, | |||||
'__wrapped__': fun}, **options))() | |||||
task = self._tasks[T.name] # return global instance. | |||||
return task | |||||
def finalize(self, auto=False): | |||||
with self._finalize_mutex: | |||||
if not self.finalized: | |||||
if auto and not self.autofinalize: | |||||
raise RuntimeError('Contract breach: app not finalized') | |||||
self.finalized = True | |||||
_announce_app_finalized(self) | |||||
pending = self._pending | |||||
while pending: | |||||
maybe_evaluate(pending.popleft()) | |||||
for task in values(self._tasks): | |||||
task.bind(self) | |||||
def add_defaults(self, fun): | |||||
if not callable(fun): | |||||
d, fun = fun, lambda: d | |||||
if self.configured: | |||||
return self.conf.add_defaults(fun()) | |||||
self._pending_defaults.append(fun) | |||||
def config_from_object(self, obj, silent=False, force=False): | |||||
self._config_source = obj | |||||
if force or self.configured: | |||||
del(self.conf) | |||||
return self.loader.config_from_object(obj, silent=silent) | |||||
def config_from_envvar(self, variable_name, silent=False, force=False): | |||||
module_name = os.environ.get(variable_name) | |||||
if not module_name: | |||||
if silent: | |||||
return False | |||||
raise ImproperlyConfigured( | |||||
ERR_ENVVAR_NOT_SET.format(variable_name)) | |||||
return self.config_from_object(module_name, silent=silent, force=force) | |||||
def config_from_cmdline(self, argv, namespace='celery'): | |||||
self.conf.update(self.loader.cmdline_config_parser(argv, namespace)) | |||||
def setup_security(self, allowed_serializers=None, key=None, cert=None, | |||||
store=None, digest='sha1', serializer='json'): | |||||
from celery.security import setup_security | |||||
return setup_security(allowed_serializers, key, cert, | |||||
store, digest, serializer, app=self) | |||||
def autodiscover_tasks(self, packages, related_name='tasks', force=False): | |||||
if force: | |||||
return self._autodiscover_tasks(packages, related_name) | |||||
signals.import_modules.connect(promise( | |||||
self._autodiscover_tasks, (packages, related_name), | |||||
), weak=False, sender=self) | |||||
def _autodiscover_tasks(self, packages, related_name='tasks', **kwargs): | |||||
# argument may be lazy | |||||
packages = packages() if callable(packages) else packages | |||||
self.loader.autodiscover_tasks(packages, related_name) | |||||
def send_task(self, name, args=None, kwargs=None, countdown=None, | |||||
eta=None, task_id=None, producer=None, connection=None, | |||||
router=None, result_cls=None, expires=None, | |||||
publisher=None, link=None, link_error=None, | |||||
add_to_parent=True, reply_to=None, **options): | |||||
task_id = task_id or uuid() | |||||
producer = producer or publisher # XXX compat | |||||
router = router or self.amqp.router | |||||
conf = self.conf | |||||
if conf.CELERY_ALWAYS_EAGER: # pragma: no cover | |||||
warnings.warn(AlwaysEagerIgnored( | |||||
'CELERY_ALWAYS_EAGER has no effect on send_task', | |||||
), stacklevel=2) | |||||
options = router.route(options, name, args, kwargs) | |||||
if connection: | |||||
producer = self.amqp.TaskProducer(connection) | |||||
with self.producer_or_acquire(producer) as P: | |||||
self.backend.on_task_call(P, task_id) | |||||
task_id = P.publish_task( | |||||
name, args, kwargs, countdown=countdown, eta=eta, | |||||
task_id=task_id, expires=expires, | |||||
callbacks=maybe_list(link), errbacks=maybe_list(link_error), | |||||
reply_to=reply_to or self.oid, **options | |||||
) | |||||
result = (result_cls or self.AsyncResult)(task_id) | |||||
if add_to_parent: | |||||
parent = get_current_worker_task() | |||||
if parent: | |||||
parent.add_trail(result) | |||||
return result | |||||
def connection(self, hostname=None, userid=None, password=None, | |||||
virtual_host=None, port=None, ssl=None, | |||||
connect_timeout=None, transport=None, | |||||
transport_options=None, heartbeat=None, | |||||
login_method=None, failover_strategy=None, **kwargs): | |||||
conf = self.conf | |||||
return self.amqp.Connection( | |||||
hostname or conf.BROKER_URL, | |||||
userid or conf.BROKER_USER, | |||||
password or conf.BROKER_PASSWORD, | |||||
virtual_host or conf.BROKER_VHOST, | |||||
port or conf.BROKER_PORT, | |||||
transport=transport or conf.BROKER_TRANSPORT, | |||||
ssl=self.either('BROKER_USE_SSL', ssl), | |||||
heartbeat=heartbeat, | |||||
login_method=login_method or conf.BROKER_LOGIN_METHOD, | |||||
failover_strategy=( | |||||
failover_strategy or conf.BROKER_FAILOVER_STRATEGY | |||||
), | |||||
transport_options=dict( | |||||
conf.BROKER_TRANSPORT_OPTIONS, **transport_options or {} | |||||
), | |||||
connect_timeout=self.either( | |||||
'BROKER_CONNECTION_TIMEOUT', connect_timeout | |||||
), | |||||
) | |||||
broker_connection = connection | |||||
def _acquire_connection(self, pool=True): | |||||
"""Helper for :meth:`connection_or_acquire`.""" | |||||
if pool: | |||||
return self.pool.acquire(block=True) | |||||
return self.connection() | |||||
def connection_or_acquire(self, connection=None, pool=True, *_, **__): | |||||
return FallbackContext(connection, self._acquire_connection, pool=pool) | |||||
default_connection = connection_or_acquire # XXX compat | |||||
def producer_or_acquire(self, producer=None): | |||||
return FallbackContext( | |||||
producer, self.amqp.producer_pool.acquire, block=True, | |||||
) | |||||
default_producer = producer_or_acquire # XXX compat | |||||
def prepare_config(self, c): | |||||
"""Prepare configuration before it is merged with the defaults.""" | |||||
return find_deprecated_settings(c) | |||||
def now(self): | |||||
return self.loader.now(utc=self.conf.CELERY_ENABLE_UTC) | |||||
def mail_admins(self, subject, body, fail_silently=False): | |||||
if self.conf.ADMINS: | |||||
to = [admin_email for _, admin_email in self.conf.ADMINS] | |||||
return self.loader.mail_admins( | |||||
subject, body, fail_silently, to=to, | |||||
sender=self.conf.SERVER_EMAIL, | |||||
host=self.conf.EMAIL_HOST, | |||||
port=self.conf.EMAIL_PORT, | |||||
user=self.conf.EMAIL_HOST_USER, | |||||
password=self.conf.EMAIL_HOST_PASSWORD, | |||||
timeout=self.conf.EMAIL_TIMEOUT, | |||||
use_ssl=self.conf.EMAIL_USE_SSL, | |||||
use_tls=self.conf.EMAIL_USE_TLS, | |||||
) | |||||
def select_queues(self, queues=None): | |||||
return self.amqp.queues.select(queues) | |||||
def either(self, default_key, *values): | |||||
"""Fallback to the value of a configuration key if none of the | |||||
`*values` are true.""" | |||||
return first(None, values) or self.conf.get(default_key) | |||||
def bugreport(self): | |||||
return bugreport(self) | |||||
def _get_backend(self): | |||||
from celery.backends import get_backend_by_url | |||||
backend, url = get_backend_by_url( | |||||
self.backend_cls or self.conf.CELERY_RESULT_BACKEND, | |||||
self.loader) | |||||
return backend(app=self, url=url) | |||||
def on_configure(self): | |||||
"""Callback calld when the app loads configuration""" | |||||
pass | |||||
def _get_config(self): | |||||
self.on_configure() | |||||
if self._config_source: | |||||
self.loader.config_from_object(self._config_source) | |||||
self.configured = True | |||||
s = Settings({}, [self.prepare_config(self.loader.conf), | |||||
deepcopy(DEFAULTS)]) | |||||
# load lazy config dict initializers. | |||||
pending = self._pending_defaults | |||||
while pending: | |||||
s.add_defaults(maybe_evaluate(pending.popleft()())) | |||||
# preconf options must be explicitly set in the conf, and not | |||||
# as defaults or they will not be pickled with the app instance. | |||||
# This will cause errors when `CELERYD_FORCE_EXECV=True` as | |||||
# the workers will not have a BROKER_URL, CELERY_RESULT_BACKEND, | |||||
# or CELERY_IMPORTS set in the config. | |||||
if self._preconf: | |||||
s.update(self._preconf) | |||||
return s | |||||
def _after_fork(self, obj_): | |||||
self._maybe_close_pool() | |||||
def _maybe_close_pool(self): | |||||
pool, self._pool = self._pool, None | |||||
if pool is not None: | |||||
pool.force_close_all() | |||||
amqp = self.__dict__.get('amqp') | |||||
if amqp is not None: | |||||
producer_pool, amqp._producer_pool = amqp._producer_pool, None | |||||
if producer_pool is not None: | |||||
producer_pool.force_close_all() | |||||
def signature(self, *args, **kwargs): | |||||
kwargs['app'] = self | |||||
return self.canvas.signature(*args, **kwargs) | |||||
def create_task_cls(self): | |||||
"""Creates a base task class using default configuration | |||||
taken from this app.""" | |||||
return self.subclass_with_self( | |||||
self.task_cls, name='Task', attribute='_app', | |||||
keep_reduce=True, abstract=True, | |||||
) | |||||
def subclass_with_self(self, Class, name=None, attribute='app', | |||||
reverse=None, keep_reduce=False, **kw): | |||||
"""Subclass an app-compatible class by setting its app attribute | |||||
to be this app instance. | |||||
App-compatible means that the class has a class attribute that | |||||
provides the default app it should use, e.g. | |||||
``class Foo: app = None``. | |||||
:param Class: The app-compatible class to subclass. | |||||
:keyword name: Custom name for the target class. | |||||
:keyword attribute: Name of the attribute holding the app, | |||||
default is 'app'. | |||||
""" | |||||
Class = symbol_by_name(Class) | |||||
reverse = reverse if reverse else Class.__name__ | |||||
def __reduce__(self): | |||||
return _unpickle_appattr, (reverse, self.__reduce_args__()) | |||||
attrs = dict({attribute: self}, __module__=Class.__module__, | |||||
__doc__=Class.__doc__, **kw) | |||||
if not keep_reduce: | |||||
attrs['__reduce__'] = __reduce__ | |||||
return type(name or Class.__name__, (Class, ), attrs) | |||||
def _rgetattr(self, path): | |||||
return attrgetter(path)(self) | |||||
def __repr__(self): | |||||
return '<{0} {1}>'.format(type(self).__name__, appstr(self)) | |||||
def __reduce__(self): | |||||
if self._using_v1_reduce: | |||||
return self.__reduce_v1__() | |||||
return (_unpickle_app_v2, (self.__class__, self.__reduce_keys__())) | |||||
def __reduce_v1__(self): | |||||
# Reduce only pickles the configuration changes, | |||||
# so the default configuration doesn't have to be passed | |||||
# between processes. | |||||
return ( | |||||
_unpickle_app, | |||||
(self.__class__, self.Pickler) + self.__reduce_args__(), | |||||
) | |||||
def __reduce_keys__(self): | |||||
"""Return keyword arguments used to reconstruct the object | |||||
when unpickling.""" | |||||
return { | |||||
'main': self.main, | |||||
'changes': self.conf.changes if self.configured else self._preconf, | |||||
'loader': self.loader_cls, | |||||
'backend': self.backend_cls, | |||||
'amqp': self.amqp_cls, | |||||
'events': self.events_cls, | |||||
'log': self.log_cls, | |||||
'control': self.control_cls, | |||||
'accept_magic_kwargs': self.accept_magic_kwargs, | |||||
'fixups': self.fixups, | |||||
'config_source': self._config_source, | |||||
'task_cls': self.task_cls, | |||||
} | |||||
def __reduce_args__(self): | |||||
"""Deprecated method, please use :meth:`__reduce_keys__` instead.""" | |||||
return (self.main, self.conf.changes, | |||||
self.loader_cls, self.backend_cls, self.amqp_cls, | |||||
self.events_cls, self.log_cls, self.control_cls, | |||||
self.accept_magic_kwargs, self._config_source) | |||||
@cached_property | |||||
def Worker(self): | |||||
return self.subclass_with_self('celery.apps.worker:Worker') | |||||
@cached_property | |||||
def WorkController(self, **kwargs): | |||||
return self.subclass_with_self('celery.worker:WorkController') | |||||
@cached_property | |||||
def Beat(self, **kwargs): | |||||
return self.subclass_with_self('celery.apps.beat:Beat') | |||||
@cached_property | |||||
def Task(self): | |||||
return self.create_task_cls() | |||||
@cached_property | |||||
def annotations(self): | |||||
return prepare_annotations(self.conf.CELERY_ANNOTATIONS) | |||||
@cached_property | |||||
def AsyncResult(self): | |||||
return self.subclass_with_self('celery.result:AsyncResult') | |||||
@cached_property | |||||
def ResultSet(self): | |||||
return self.subclass_with_self('celery.result:ResultSet') | |||||
@cached_property | |||||
def GroupResult(self): | |||||
return self.subclass_with_self('celery.result:GroupResult') | |||||
@cached_property | |||||
def TaskSet(self): # XXX compat | |||||
"""Deprecated! Please use :class:`celery.group` instead.""" | |||||
return self.subclass_with_self('celery.task.sets:TaskSet') | |||||
@cached_property | |||||
def TaskSetResult(self): # XXX compat | |||||
"""Deprecated! Please use :attr:`GroupResult` instead.""" | |||||
return self.subclass_with_self('celery.result:TaskSetResult') | |||||
@property | |||||
def pool(self): | |||||
if self._pool is None: | |||||
_ensure_after_fork() | |||||
limit = self.conf.BROKER_POOL_LIMIT | |||||
self._pool = self.connection().Pool(limit=limit) | |||||
return self._pool | |||||
@property | |||||
def current_task(self): | |||||
return _task_stack.top | |||||
@cached_property | |||||
def oid(self): | |||||
return oid_from(self) | |||||
@cached_property | |||||
def amqp(self): | |||||
return instantiate(self.amqp_cls, app=self) | |||||
@cached_property | |||||
def backend(self): | |||||
return self._get_backend() | |||||
@cached_property | |||||
def conf(self): | |||||
return self._get_config() | |||||
@cached_property | |||||
def control(self): | |||||
return instantiate(self.control_cls, app=self) | |||||
@cached_property | |||||
def events(self): | |||||
return instantiate(self.events_cls, app=self) | |||||
@cached_property | |||||
def loader(self): | |||||
return get_loader_cls(self.loader_cls)(app=self) | |||||
@cached_property | |||||
def log(self): | |||||
return instantiate(self.log_cls, app=self) | |||||
@cached_property | |||||
def canvas(self): | |||||
from celery import canvas | |||||
return canvas | |||||
@cached_property | |||||
def tasks(self): | |||||
self.finalize(auto=True) | |||||
return self._tasks | |||||
@cached_property | |||||
def timezone(self): | |||||
from celery.utils.timeutils import timezone | |||||
conf = self.conf | |||||
tz = conf.CELERY_TIMEZONE | |||||
if not tz: | |||||
return (timezone.get_timezone('UTC') if conf.CELERY_ENABLE_UTC | |||||
else timezone.local) | |||||
return timezone.get_timezone(self.conf.CELERY_TIMEZONE) | |||||
App = Celery # compat |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.builtins | |||||
~~~~~~~~~~~~~~~~~~~ | |||||
Built-in tasks that are always available in all | |||||
app instances. E.g. chord, group and xmap. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from collections import deque | |||||
from celery._state import get_current_worker_task, connect_on_app_finalize | |||||
from celery.utils import uuid | |||||
from celery.utils.log import get_logger | |||||
__all__ = [] | |||||
logger = get_logger(__name__) | |||||
@connect_on_app_finalize | |||||
def add_backend_cleanup_task(app): | |||||
"""The backend cleanup task can be used to clean up the default result | |||||
backend. | |||||
If the configured backend requires periodic cleanup this task is also | |||||
automatically configured to run every day at 4am (requires | |||||
:program:`celery beat` to be running). | |||||
""" | |||||
@app.task(name='celery.backend_cleanup', | |||||
shared=False, _force_evaluate=True) | |||||
def backend_cleanup(): | |||||
app.backend.cleanup() | |||||
return backend_cleanup | |||||
@connect_on_app_finalize | |||||
def add_unlock_chord_task(app): | |||||
"""This task is used by result backends without native chord support. | |||||
It joins chords by creating a task chain polling the header for completion. | |||||
""" | |||||
from celery.canvas import signature | |||||
from celery.exceptions import ChordError | |||||
from celery.result import allow_join_result, result_from_tuple | |||||
default_propagate = app.conf.CELERY_CHORD_PROPAGATES | |||||
@app.task(name='celery.chord_unlock', max_retries=None, shared=False, | |||||
default_retry_delay=1, ignore_result=True, _force_evaluate=True, | |||||
bind=True) | |||||
def unlock_chord(self, group_id, callback, interval=None, propagate=None, | |||||
max_retries=None, result=None, | |||||
Result=app.AsyncResult, GroupResult=app.GroupResult, | |||||
result_from_tuple=result_from_tuple): | |||||
# if propagate is disabled exceptions raised by chord tasks | |||||
# will be sent as part of the result list to the chord callback. | |||||
# Since 3.1 propagate will be enabled by default, and instead | |||||
# the chord callback changes state to FAILURE with the | |||||
# exception set to ChordError. | |||||
propagate = default_propagate if propagate is None else propagate | |||||
if interval is None: | |||||
interval = self.default_retry_delay | |||||
# check if the task group is ready, and if so apply the callback. | |||||
deps = GroupResult( | |||||
group_id, | |||||
[result_from_tuple(r, app=app) for r in result], | |||||
app=app, | |||||
) | |||||
j = deps.join_native if deps.supports_native_join else deps.join | |||||
try: | |||||
ready = deps.ready() | |||||
except Exception as exc: | |||||
raise self.retry( | |||||
exc=exc, countdown=interval, max_retries=max_retries, | |||||
) | |||||
else: | |||||
if not ready: | |||||
raise self.retry(countdown=interval, max_retries=max_retries) | |||||
callback = signature(callback, app=app) | |||||
try: | |||||
with allow_join_result(): | |||||
ret = j(timeout=3.0, propagate=propagate) | |||||
except Exception as exc: | |||||
try: | |||||
culprit = next(deps._failed_join_report()) | |||||
reason = 'Dependency {0.id} raised {1!r}'.format( | |||||
culprit, exc, | |||||
) | |||||
except StopIteration: | |||||
reason = repr(exc) | |||||
logger.error('Chord %r raised: %r', group_id, exc, exc_info=1) | |||||
app.backend.chord_error_from_stack(callback, | |||||
ChordError(reason)) | |||||
else: | |||||
try: | |||||
callback.delay(ret) | |||||
except Exception as exc: | |||||
logger.error('Chord %r raised: %r', group_id, exc, exc_info=1) | |||||
app.backend.chord_error_from_stack( | |||||
callback, | |||||
exc=ChordError('Callback error: {0!r}'.format(exc)), | |||||
) | |||||
return unlock_chord | |||||
@connect_on_app_finalize | |||||
def add_map_task(app): | |||||
from celery.canvas import signature | |||||
@app.task(name='celery.map', shared=False, _force_evaluate=True) | |||||
def xmap(task, it): | |||||
task = signature(task, app=app).type | |||||
return [task(item) for item in it] | |||||
return xmap | |||||
@connect_on_app_finalize | |||||
def add_starmap_task(app): | |||||
from celery.canvas import signature | |||||
@app.task(name='celery.starmap', shared=False, _force_evaluate=True) | |||||
def xstarmap(task, it): | |||||
task = signature(task, app=app).type | |||||
return [task(*item) for item in it] | |||||
return xstarmap | |||||
@connect_on_app_finalize | |||||
def add_chunk_task(app): | |||||
from celery.canvas import chunks as _chunks | |||||
@app.task(name='celery.chunks', shared=False, _force_evaluate=True) | |||||
def chunks(task, it, n): | |||||
return _chunks.apply_chunks(task, it, n) | |||||
return chunks | |||||
@connect_on_app_finalize | |||||
def add_group_task(app): | |||||
_app = app | |||||
from celery.canvas import maybe_signature, signature | |||||
from celery.result import result_from_tuple | |||||
class Group(app.Task): | |||||
app = _app | |||||
name = 'celery.group' | |||||
accept_magic_kwargs = False | |||||
_decorated = True | |||||
def run(self, tasks, result, group_id, partial_args, | |||||
add_to_parent=True): | |||||
app = self.app | |||||
result = result_from_tuple(result, app) | |||||
# any partial args are added to all tasks in the group | |||||
taskit = (signature(task, app=app).clone(partial_args) | |||||
for i, task in enumerate(tasks)) | |||||
if self.request.is_eager or app.conf.CELERY_ALWAYS_EAGER: | |||||
return app.GroupResult( | |||||
result.id, | |||||
[stask.apply(group_id=group_id) for stask in taskit], | |||||
) | |||||
with app.producer_or_acquire() as pub: | |||||
[stask.apply_async(group_id=group_id, producer=pub, | |||||
add_to_parent=False) for stask in taskit] | |||||
parent = get_current_worker_task() | |||||
if add_to_parent and parent: | |||||
parent.add_trail(result) | |||||
return result | |||||
def prepare(self, options, tasks, args, **kwargs): | |||||
options['group_id'] = group_id = ( | |||||
options.setdefault('task_id', uuid())) | |||||
def prepare_member(task): | |||||
task = maybe_signature(task, app=self.app) | |||||
task.options['group_id'] = group_id | |||||
return task, task.freeze() | |||||
try: | |||||
tasks, res = list(zip( | |||||
*[prepare_member(task) for task in tasks] | |||||
)) | |||||
except ValueError: # tasks empty | |||||
tasks, res = [], [] | |||||
return (tasks, self.app.GroupResult(group_id, res), group_id, args) | |||||
def apply_async(self, partial_args=(), kwargs={}, **options): | |||||
if self.app.conf.CELERY_ALWAYS_EAGER: | |||||
return self.apply(partial_args, kwargs, **options) | |||||
tasks, result, gid, args = self.prepare( | |||||
options, args=partial_args, **kwargs | |||||
) | |||||
super(Group, self).apply_async(( | |||||
list(tasks), result.as_tuple(), gid, args), **options | |||||
) | |||||
return result | |||||
def apply(self, args=(), kwargs={}, **options): | |||||
return super(Group, self).apply( | |||||
self.prepare(options, args=args, **kwargs), | |||||
**options).get() | |||||
return Group | |||||
@connect_on_app_finalize | |||||
def add_chain_task(app): | |||||
from celery.canvas import ( | |||||
Signature, chain, chord, group, maybe_signature, maybe_unroll_group, | |||||
) | |||||
_app = app | |||||
class Chain(app.Task): | |||||
app = _app | |||||
name = 'celery.chain' | |||||
accept_magic_kwargs = False | |||||
_decorated = True | |||||
def prepare_steps(self, args, tasks): | |||||
app = self.app | |||||
steps = deque(tasks) | |||||
next_step = prev_task = prev_res = None | |||||
tasks, results = [], [] | |||||
i = 0 | |||||
while steps: | |||||
# First task get partial args from chain. | |||||
task = maybe_signature(steps.popleft(), app=app) | |||||
task = task.clone() if i else task.clone(args) | |||||
res = task.freeze() | |||||
i += 1 | |||||
if isinstance(task, group): | |||||
task = maybe_unroll_group(task) | |||||
if isinstance(task, chain): | |||||
# splice the chain | |||||
steps.extendleft(reversed(task.tasks)) | |||||
continue | |||||
elif isinstance(task, group) and steps and \ | |||||
not isinstance(steps[0], group): | |||||
# automatically upgrade group(..) | s to chord(group, s) | |||||
try: | |||||
next_step = steps.popleft() | |||||
# for chords we freeze by pretending it's a normal | |||||
# task instead of a group. | |||||
res = Signature.freeze(next_step) | |||||
task = chord(task, body=next_step, task_id=res.task_id) | |||||
except IndexError: | |||||
pass # no callback, so keep as group | |||||
if prev_task: | |||||
# link previous task to this task. | |||||
prev_task.link(task) | |||||
# set the results parent attribute. | |||||
if not res.parent: | |||||
res.parent = prev_res | |||||
if not isinstance(prev_task, chord): | |||||
results.append(res) | |||||
tasks.append(task) | |||||
prev_task, prev_res = task, res | |||||
return tasks, results | |||||
def apply_async(self, args=(), kwargs={}, group_id=None, chord=None, | |||||
task_id=None, link=None, link_error=None, **options): | |||||
if self.app.conf.CELERY_ALWAYS_EAGER: | |||||
return self.apply(args, kwargs, **options) | |||||
options.pop('publisher', None) | |||||
tasks, results = self.prepare_steps(args, kwargs['tasks']) | |||||
result = results[-1] | |||||
if group_id: | |||||
tasks[-1].set(group_id=group_id) | |||||
if chord: | |||||
tasks[-1].set(chord=chord) | |||||
if task_id: | |||||
tasks[-1].set(task_id=task_id) | |||||
result = tasks[-1].type.AsyncResult(task_id) | |||||
# make sure we can do a link() and link_error() on a chain object. | |||||
if link: | |||||
tasks[-1].set(link=link) | |||||
# and if any task in the chain fails, call the errbacks | |||||
if link_error: | |||||
for task in tasks: | |||||
task.set(link_error=link_error) | |||||
tasks[0].apply_async(**options) | |||||
return result | |||||
def apply(self, args=(), kwargs={}, signature=maybe_signature, | |||||
**options): | |||||
app = self.app | |||||
last, fargs = None, args # fargs passed to first task only | |||||
for task in kwargs['tasks']: | |||||
res = signature(task, app=app).clone(fargs).apply( | |||||
last and (last.get(), ), | |||||
) | |||||
res.parent, last, fargs = last, res, None | |||||
return last | |||||
return Chain | |||||
@connect_on_app_finalize | |||||
def add_chord_task(app): | |||||
"""Every chord is executed in a dedicated task, so that the chord | |||||
can be used as a signature, and this generates the task | |||||
responsible for that.""" | |||||
from celery import group | |||||
from celery.canvas import maybe_signature | |||||
_app = app | |||||
default_propagate = app.conf.CELERY_CHORD_PROPAGATES | |||||
class Chord(app.Task): | |||||
app = _app | |||||
name = 'celery.chord' | |||||
accept_magic_kwargs = False | |||||
ignore_result = False | |||||
_decorated = True | |||||
def run(self, header, body, partial_args=(), interval=None, | |||||
countdown=1, max_retries=None, propagate=None, | |||||
eager=False, **kwargs): | |||||
app = self.app | |||||
propagate = default_propagate if propagate is None else propagate | |||||
group_id = uuid() | |||||
# - convert back to group if serialized | |||||
tasks = header.tasks if isinstance(header, group) else header | |||||
header = group([ | |||||
maybe_signature(s, app=app).clone() for s in tasks | |||||
], app=self.app) | |||||
# - eager applies the group inline | |||||
if eager: | |||||
return header.apply(args=partial_args, task_id=group_id) | |||||
body['chord_size'] = len(header.tasks) | |||||
results = header.freeze(group_id=group_id, chord=body).results | |||||
return self.backend.apply_chord( | |||||
header, partial_args, group_id, | |||||
body, interval=interval, countdown=countdown, | |||||
max_retries=max_retries, propagate=propagate, result=results, | |||||
) | |||||
def apply_async(self, args=(), kwargs={}, task_id=None, | |||||
group_id=None, chord=None, **options): | |||||
app = self.app | |||||
if app.conf.CELERY_ALWAYS_EAGER: | |||||
return self.apply(args, kwargs, **options) | |||||
header = kwargs.pop('header') | |||||
body = kwargs.pop('body') | |||||
header, body = (maybe_signature(header, app=app), | |||||
maybe_signature(body, app=app)) | |||||
# forward certain options to body | |||||
if chord is not None: | |||||
body.options['chord'] = chord | |||||
if group_id is not None: | |||||
body.options['group_id'] = group_id | |||||
[body.link(s) for s in options.pop('link', [])] | |||||
[body.link_error(s) for s in options.pop('link_error', [])] | |||||
body_result = body.freeze(task_id) | |||||
parent = super(Chord, self).apply_async((header, body, args), | |||||
kwargs, **options) | |||||
body_result.parent = parent | |||||
return body_result | |||||
def apply(self, args=(), kwargs={}, propagate=True, **options): | |||||
body = kwargs['body'] | |||||
res = super(Chord, self).apply(args, dict(kwargs, eager=True), | |||||
**options) | |||||
return maybe_signature(body, app=self.app).apply( | |||||
args=(res.get(propagate=propagate).get(), )) | |||||
return Chord |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.control | |||||
~~~~~~~~~~~~~~~~~~~ | |||||
Client for worker remote control commands. | |||||
Server implementation is in :mod:`celery.worker.control`. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import warnings | |||||
from kombu.pidbox import Mailbox | |||||
from kombu.utils import cached_property | |||||
from celery.exceptions import DuplicateNodenameWarning | |||||
from celery.utils.text import pluralize | |||||
__all__ = ['Inspect', 'Control', 'flatten_reply'] | |||||
W_DUPNODE = """\ | |||||
Received multiple replies from node {0}: {1}. | |||||
Please make sure you give each node a unique nodename using the `-n` option.\ | |||||
""" | |||||
def flatten_reply(reply): | |||||
nodes, dupes = {}, set() | |||||
for item in reply: | |||||
[dupes.add(name) for name in item if name in nodes] | |||||
nodes.update(item) | |||||
if dupes: | |||||
warnings.warn(DuplicateNodenameWarning( | |||||
W_DUPNODE.format( | |||||
pluralize(len(dupes), 'name'), ', '.join(sorted(dupes)), | |||||
), | |||||
)) | |||||
return nodes | |||||
class Inspect(object): | |||||
app = None | |||||
def __init__(self, destination=None, timeout=1, callback=None, | |||||
connection=None, app=None, limit=None): | |||||
self.app = app or self.app | |||||
self.destination = destination | |||||
self.timeout = timeout | |||||
self.callback = callback | |||||
self.connection = connection | |||||
self.limit = limit | |||||
def _prepare(self, reply): | |||||
if not reply: | |||||
return | |||||
by_node = flatten_reply(reply) | |||||
if self.destination and \ | |||||
not isinstance(self.destination, (list, tuple)): | |||||
return by_node.get(self.destination) | |||||
return by_node | |||||
def _request(self, command, **kwargs): | |||||
return self._prepare(self.app.control.broadcast( | |||||
command, | |||||
arguments=kwargs, | |||||
destination=self.destination, | |||||
callback=self.callback, | |||||
connection=self.connection, | |||||
limit=self.limit, | |||||
timeout=self.timeout, reply=True, | |||||
)) | |||||
def report(self): | |||||
return self._request('report') | |||||
def clock(self): | |||||
return self._request('clock') | |||||
def active(self, safe=False): | |||||
return self._request('dump_active', safe=safe) | |||||
def scheduled(self, safe=False): | |||||
return self._request('dump_schedule', safe=safe) | |||||
def reserved(self, safe=False): | |||||
return self._request('dump_reserved', safe=safe) | |||||
def stats(self): | |||||
return self._request('stats') | |||||
def revoked(self): | |||||
return self._request('dump_revoked') | |||||
def registered(self, *taskinfoitems): | |||||
return self._request('dump_tasks', taskinfoitems=taskinfoitems) | |||||
registered_tasks = registered | |||||
def ping(self): | |||||
return self._request('ping') | |||||
def active_queues(self): | |||||
return self._request('active_queues') | |||||
def query_task(self, ids): | |||||
return self._request('query_task', ids=ids) | |||||
def conf(self, with_defaults=False): | |||||
return self._request('dump_conf', with_defaults=with_defaults) | |||||
def hello(self, from_node, revoked=None): | |||||
return self._request('hello', from_node=from_node, revoked=revoked) | |||||
def memsample(self): | |||||
return self._request('memsample') | |||||
def memdump(self, samples=10): | |||||
return self._request('memdump', samples=samples) | |||||
def objgraph(self, type='Request', n=200, max_depth=10): | |||||
return self._request('objgraph', num=n, max_depth=max_depth, type=type) | |||||
class Control(object): | |||||
Mailbox = Mailbox | |||||
def __init__(self, app=None): | |||||
self.app = app | |||||
self.mailbox = self.Mailbox('celery', type='fanout', accept=['json']) | |||||
@cached_property | |||||
def inspect(self): | |||||
return self.app.subclass_with_self(Inspect, reverse='control.inspect') | |||||
def purge(self, connection=None): | |||||
"""Discard all waiting tasks. | |||||
This will ignore all tasks waiting for execution, and they will | |||||
be deleted from the messaging server. | |||||
:returns: the number of tasks discarded. | |||||
""" | |||||
with self.app.connection_or_acquire(connection) as conn: | |||||
return self.app.amqp.TaskConsumer(conn).purge() | |||||
discard_all = purge | |||||
def election(self, id, topic, action=None, connection=None): | |||||
self.broadcast('election', connection=connection, arguments={ | |||||
'id': id, 'topic': topic, 'action': action, | |||||
}) | |||||
def revoke(self, task_id, destination=None, terminate=False, | |||||
signal='SIGTERM', **kwargs): | |||||
"""Tell all (or specific) workers to revoke a task by id. | |||||
If a task is revoked, the workers will ignore the task and | |||||
not execute it after all. | |||||
:param task_id: Id of the task to revoke. | |||||
:keyword terminate: Also terminate the process currently working | |||||
on the task (if any). | |||||
:keyword signal: Name of signal to send to process if terminate. | |||||
Default is TERM. | |||||
See :meth:`broadcast` for supported keyword arguments. | |||||
""" | |||||
return self.broadcast('revoke', destination=destination, | |||||
arguments={'task_id': task_id, | |||||
'terminate': terminate, | |||||
'signal': signal}, **kwargs) | |||||
def ping(self, destination=None, timeout=1, **kwargs): | |||||
"""Ping all (or specific) workers. | |||||
Will return the list of answers. | |||||
See :meth:`broadcast` for supported keyword arguments. | |||||
""" | |||||
return self.broadcast('ping', reply=True, destination=destination, | |||||
timeout=timeout, **kwargs) | |||||
def rate_limit(self, task_name, rate_limit, destination=None, **kwargs): | |||||
"""Tell all (or specific) workers to set a new rate limit | |||||
for task by type. | |||||
:param task_name: Name of task to change rate limit for. | |||||
:param rate_limit: The rate limit as tasks per second, or a rate limit | |||||
string (`'100/m'`, etc. | |||||
see :attr:`celery.task.base.Task.rate_limit` for | |||||
more information). | |||||
See :meth:`broadcast` for supported keyword arguments. | |||||
""" | |||||
return self.broadcast('rate_limit', destination=destination, | |||||
arguments={'task_name': task_name, | |||||
'rate_limit': rate_limit}, | |||||
**kwargs) | |||||
def add_consumer(self, queue, exchange=None, exchange_type='direct', | |||||
routing_key=None, options=None, **kwargs): | |||||
"""Tell all (or specific) workers to start consuming from a new queue. | |||||
Only the queue name is required as if only the queue is specified | |||||
then the exchange/routing key will be set to the same name ( | |||||
like automatic queues do). | |||||
.. note:: | |||||
This command does not respect the default queue/exchange | |||||
options in the configuration. | |||||
:param queue: Name of queue to start consuming from. | |||||
:keyword exchange: Optional name of exchange. | |||||
:keyword exchange_type: Type of exchange (defaults to 'direct') | |||||
command to, when empty broadcast to all workers. | |||||
:keyword routing_key: Optional routing key. | |||||
:keyword options: Additional options as supported | |||||
by :meth:`kombu.entitiy.Queue.from_dict`. | |||||
See :meth:`broadcast` for supported keyword arguments. | |||||
""" | |||||
return self.broadcast( | |||||
'add_consumer', | |||||
arguments=dict({'queue': queue, 'exchange': exchange, | |||||
'exchange_type': exchange_type, | |||||
'routing_key': routing_key}, **options or {}), | |||||
**kwargs | |||||
) | |||||
def cancel_consumer(self, queue, **kwargs): | |||||
"""Tell all (or specific) workers to stop consuming from ``queue``. | |||||
Supports the same keyword arguments as :meth:`broadcast`. | |||||
""" | |||||
return self.broadcast( | |||||
'cancel_consumer', arguments={'queue': queue}, **kwargs | |||||
) | |||||
def time_limit(self, task_name, soft=None, hard=None, **kwargs): | |||||
"""Tell all (or specific) workers to set time limits for | |||||
a task by type. | |||||
:param task_name: Name of task to change time limits for. | |||||
:keyword soft: New soft time limit (in seconds). | |||||
:keyword hard: New hard time limit (in seconds). | |||||
Any additional keyword arguments are passed on to :meth:`broadcast`. | |||||
""" | |||||
return self.broadcast( | |||||
'time_limit', | |||||
arguments={'task_name': task_name, | |||||
'hard': hard, 'soft': soft}, **kwargs) | |||||
def enable_events(self, destination=None, **kwargs): | |||||
"""Tell all (or specific) workers to enable events.""" | |||||
return self.broadcast('enable_events', {}, destination, **kwargs) | |||||
def disable_events(self, destination=None, **kwargs): | |||||
"""Tell all (or specific) workers to disable events.""" | |||||
return self.broadcast('disable_events', {}, destination, **kwargs) | |||||
def pool_grow(self, n=1, destination=None, **kwargs): | |||||
"""Tell all (or specific) workers to grow the pool by ``n``. | |||||
Supports the same arguments as :meth:`broadcast`. | |||||
""" | |||||
return self.broadcast('pool_grow', {'n': n}, destination, **kwargs) | |||||
def pool_shrink(self, n=1, destination=None, **kwargs): | |||||
"""Tell all (or specific) workers to shrink the pool by ``n``. | |||||
Supports the same arguments as :meth:`broadcast`. | |||||
""" | |||||
return self.broadcast('pool_shrink', {'n': n}, destination, **kwargs) | |||||
def autoscale(self, max, min, destination=None, **kwargs): | |||||
"""Change worker(s) autoscale setting. | |||||
Supports the same arguments as :meth:`broadcast`. | |||||
""" | |||||
return self.broadcast( | |||||
'autoscale', {'max': max, 'min': min}, destination, **kwargs) | |||||
def broadcast(self, command, arguments=None, destination=None, | |||||
connection=None, reply=False, timeout=1, limit=None, | |||||
callback=None, channel=None, **extra_kwargs): | |||||
"""Broadcast a control command to the celery workers. | |||||
:param command: Name of command to send. | |||||
:param arguments: Keyword arguments for the command. | |||||
:keyword destination: If set, a list of the hosts to send the | |||||
command to, when empty broadcast to all workers. | |||||
:keyword connection: Custom broker connection to use, if not set, | |||||
a connection will be established automatically. | |||||
:keyword reply: Wait for and return the reply. | |||||
:keyword timeout: Timeout in seconds to wait for the reply. | |||||
:keyword limit: Limit number of replies. | |||||
:keyword callback: Callback called immediately for each reply | |||||
received. | |||||
""" | |||||
with self.app.connection_or_acquire(connection) as conn: | |||||
arguments = dict(arguments or {}, **extra_kwargs) | |||||
return self.mailbox(conn)._broadcast( | |||||
command, arguments, destination, reply, timeout, | |||||
limit, callback, channel=channel, | |||||
) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.defaults | |||||
~~~~~~~~~~~~~~~~~~~ | |||||
Configuration introspection and defaults. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import sys | |||||
from collections import deque, namedtuple | |||||
from datetime import timedelta | |||||
from celery.five import items | |||||
from celery.utils import strtobool | |||||
from celery.utils.functional import memoize | |||||
__all__ = ['Option', 'NAMESPACES', 'flatten', 'find'] | |||||
is_jython = sys.platform.startswith('java') | |||||
is_pypy = hasattr(sys, 'pypy_version_info') | |||||
DEFAULT_POOL = 'prefork' | |||||
if is_jython: | |||||
DEFAULT_POOL = 'threads' | |||||
elif is_pypy: | |||||
if sys.pypy_version_info[0:3] < (1, 5, 0): | |||||
DEFAULT_POOL = 'solo' | |||||
else: | |||||
DEFAULT_POOL = 'prefork' | |||||
DEFAULT_ACCEPT_CONTENT = ['json', 'pickle', 'msgpack', 'yaml'] | |||||
DEFAULT_PROCESS_LOG_FMT = """ | |||||
[%(asctime)s: %(levelname)s/%(processName)s] %(message)s | |||||
""".strip() | |||||
DEFAULT_LOG_FMT = '[%(asctime)s: %(levelname)s] %(message)s' | |||||
DEFAULT_TASK_LOG_FMT = """[%(asctime)s: %(levelname)s/%(processName)s] \ | |||||
%(task_name)s[%(task_id)s]: %(message)s""" | |||||
_BROKER_OLD = {'deprecate_by': '2.5', 'remove_by': '4.0', | |||||
'alt': 'BROKER_URL setting'} | |||||
_REDIS_OLD = {'deprecate_by': '2.5', 'remove_by': '4.0', | |||||
'alt': 'URL form of CELERY_RESULT_BACKEND'} | |||||
searchresult = namedtuple('searchresult', ('namespace', 'key', 'type')) | |||||
# logging: processName first introduced in Py 2.6.2 (Issue #1644). | |||||
if sys.version_info < (2, 6, 2): | |||||
DEFAULT_PROCESS_LOG_FMT = DEFAULT_LOG_FMT | |||||
class Option(object): | |||||
alt = None | |||||
deprecate_by = None | |||||
remove_by = None | |||||
typemap = dict(string=str, int=int, float=float, any=lambda v: v, | |||||
bool=strtobool, dict=dict, tuple=tuple) | |||||
def __init__(self, default=None, *args, **kwargs): | |||||
self.default = default | |||||
self.type = kwargs.get('type') or 'string' | |||||
for attr, value in items(kwargs): | |||||
setattr(self, attr, value) | |||||
def to_python(self, value): | |||||
return self.typemap[self.type](value) | |||||
def __repr__(self): | |||||
return '<Option: type->{0} default->{1!r}>'.format(self.type, | |||||
self.default) | |||||
NAMESPACES = { | |||||
'BROKER': { | |||||
'URL': Option(None, type='string'), | |||||
'CONNECTION_TIMEOUT': Option(4, type='float'), | |||||
'CONNECTION_RETRY': Option(True, type='bool'), | |||||
'CONNECTION_MAX_RETRIES': Option(100, type='int'), | |||||
'FAILOVER_STRATEGY': Option(None, type='string'), | |||||
'HEARTBEAT': Option(None, type='int'), | |||||
'HEARTBEAT_CHECKRATE': Option(3.0, type='int'), | |||||
'LOGIN_METHOD': Option(None, type='string'), | |||||
'POOL_LIMIT': Option(10, type='int'), | |||||
'USE_SSL': Option(False, type='bool'), | |||||
'TRANSPORT': Option(type='string'), | |||||
'TRANSPORT_OPTIONS': Option({}, type='dict'), | |||||
'HOST': Option(type='string', **_BROKER_OLD), | |||||
'PORT': Option(type='int', **_BROKER_OLD), | |||||
'USER': Option(type='string', **_BROKER_OLD), | |||||
'PASSWORD': Option(type='string', **_BROKER_OLD), | |||||
'VHOST': Option(type='string', **_BROKER_OLD), | |||||
}, | |||||
'CASSANDRA': { | |||||
'COLUMN_FAMILY': Option(type='string'), | |||||
'DETAILED_MODE': Option(False, type='bool'), | |||||
'KEYSPACE': Option(type='string'), | |||||
'READ_CONSISTENCY': Option(type='string'), | |||||
'SERVERS': Option(type='list'), | |||||
'WRITE_CONSISTENCY': Option(type='string'), | |||||
}, | |||||
'CELERY': { | |||||
'ACCEPT_CONTENT': Option(DEFAULT_ACCEPT_CONTENT, type='list'), | |||||
'ACKS_LATE': Option(False, type='bool'), | |||||
'ALWAYS_EAGER': Option(False, type='bool'), | |||||
'ANNOTATIONS': Option(type='any'), | |||||
'BROADCAST_QUEUE': Option('celeryctl'), | |||||
'BROADCAST_EXCHANGE': Option('celeryctl'), | |||||
'BROADCAST_EXCHANGE_TYPE': Option('fanout'), | |||||
'CACHE_BACKEND': Option(), | |||||
'CACHE_BACKEND_OPTIONS': Option({}, type='dict'), | |||||
'CHORD_PROPAGATES': Option(True, type='bool'), | |||||
'COUCHBASE_BACKEND_SETTINGS': Option(None, type='dict'), | |||||
'CREATE_MISSING_QUEUES': Option(True, type='bool'), | |||||
'DEFAULT_RATE_LIMIT': Option(type='string'), | |||||
'DISABLE_RATE_LIMITS': Option(False, type='bool'), | |||||
'DEFAULT_ROUTING_KEY': Option('celery'), | |||||
'DEFAULT_QUEUE': Option('celery'), | |||||
'DEFAULT_EXCHANGE': Option('celery'), | |||||
'DEFAULT_EXCHANGE_TYPE': Option('direct'), | |||||
'DEFAULT_DELIVERY_MODE': Option(2, type='string'), | |||||
'EAGER_PROPAGATES_EXCEPTIONS': Option(False, type='bool'), | |||||
'ENABLE_UTC': Option(True, type='bool'), | |||||
'ENABLE_REMOTE_CONTROL': Option(True, type='bool'), | |||||
'EVENT_SERIALIZER': Option('json'), | |||||
'EVENT_QUEUE_EXPIRES': Option(None, type='float'), | |||||
'EVENT_QUEUE_TTL': Option(None, type='float'), | |||||
'IMPORTS': Option((), type='tuple'), | |||||
'INCLUDE': Option((), type='tuple'), | |||||
'IGNORE_RESULT': Option(False, type='bool'), | |||||
'MAX_CACHED_RESULTS': Option(100, type='int'), | |||||
'MESSAGE_COMPRESSION': Option(type='string'), | |||||
'MONGODB_BACKEND_SETTINGS': Option(type='dict'), | |||||
'REDIS_HOST': Option(type='string', **_REDIS_OLD), | |||||
'REDIS_PORT': Option(type='int', **_REDIS_OLD), | |||||
'REDIS_DB': Option(type='int', **_REDIS_OLD), | |||||
'REDIS_PASSWORD': Option(type='string', **_REDIS_OLD), | |||||
'REDIS_MAX_CONNECTIONS': Option(type='int'), | |||||
'RESULT_BACKEND': Option(type='string'), | |||||
'RESULT_DB_SHORT_LIVED_SESSIONS': Option(False, type='bool'), | |||||
'RESULT_DB_TABLENAMES': Option(type='dict'), | |||||
'RESULT_DBURI': Option(), | |||||
'RESULT_ENGINE_OPTIONS': Option(type='dict'), | |||||
'RESULT_EXCHANGE': Option('celeryresults'), | |||||
'RESULT_EXCHANGE_TYPE': Option('direct'), | |||||
'RESULT_SERIALIZER': Option('pickle'), | |||||
'RESULT_PERSISTENT': Option(None, type='bool'), | |||||
'ROUTES': Option(type='any'), | |||||
'SEND_EVENTS': Option(False, type='bool'), | |||||
'SEND_TASK_ERROR_EMAILS': Option(False, type='bool'), | |||||
'SEND_TASK_SENT_EVENT': Option(False, type='bool'), | |||||
'STORE_ERRORS_EVEN_IF_IGNORED': Option(False, type='bool'), | |||||
'TASK_PUBLISH_RETRY': Option(True, type='bool'), | |||||
'TASK_PUBLISH_RETRY_POLICY': Option({ | |||||
'max_retries': 3, | |||||
'interval_start': 0, | |||||
'interval_max': 1, | |||||
'interval_step': 0.2}, type='dict'), | |||||
'TASK_RESULT_EXPIRES': Option(timedelta(days=1), type='float'), | |||||
'TASK_SERIALIZER': Option('pickle'), | |||||
'TIMEZONE': Option(type='string'), | |||||
'TRACK_STARTED': Option(False, type='bool'), | |||||
'REDIRECT_STDOUTS': Option(True, type='bool'), | |||||
'REDIRECT_STDOUTS_LEVEL': Option('WARNING'), | |||||
'QUEUES': Option(type='dict'), | |||||
'QUEUE_HA_POLICY': Option(None, type='string'), | |||||
'SECURITY_KEY': Option(type='string'), | |||||
'SECURITY_CERTIFICATE': Option(type='string'), | |||||
'SECURITY_CERT_STORE': Option(type='string'), | |||||
'WORKER_DIRECT': Option(False, type='bool'), | |||||
}, | |||||
'CELERYD': { | |||||
'AGENT': Option(None, type='string'), | |||||
'AUTOSCALER': Option('celery.worker.autoscale:Autoscaler'), | |||||
'AUTORELOADER': Option('celery.worker.autoreload:Autoreloader'), | |||||
'CONCURRENCY': Option(0, type='int'), | |||||
'TIMER': Option(type='string'), | |||||
'TIMER_PRECISION': Option(1.0, type='float'), | |||||
'FORCE_EXECV': Option(False, type='bool'), | |||||
'HIJACK_ROOT_LOGGER': Option(True, type='bool'), | |||||
'CONSUMER': Option('celery.worker.consumer:Consumer', type='string'), | |||||
'LOG_FORMAT': Option(DEFAULT_PROCESS_LOG_FMT), | |||||
'LOG_COLOR': Option(type='bool'), | |||||
'LOG_LEVEL': Option('WARN', deprecate_by='2.4', remove_by='4.0', | |||||
alt='--loglevel argument'), | |||||
'LOG_FILE': Option(deprecate_by='2.4', remove_by='4.0', | |||||
alt='--logfile argument'), | |||||
'MAX_TASKS_PER_CHILD': Option(type='int'), | |||||
'POOL': Option(DEFAULT_POOL), | |||||
'POOL_PUTLOCKS': Option(True, type='bool'), | |||||
'POOL_RESTARTS': Option(False, type='bool'), | |||||
'PREFETCH_MULTIPLIER': Option(4, type='int'), | |||||
'STATE_DB': Option(), | |||||
'TASK_LOG_FORMAT': Option(DEFAULT_TASK_LOG_FMT), | |||||
'TASK_SOFT_TIME_LIMIT': Option(type='float'), | |||||
'TASK_TIME_LIMIT': Option(type='float'), | |||||
'WORKER_LOST_WAIT': Option(10.0, type='float') | |||||
}, | |||||
'CELERYBEAT': { | |||||
'SCHEDULE': Option({}, type='dict'), | |||||
'SCHEDULER': Option('celery.beat:PersistentScheduler'), | |||||
'SCHEDULE_FILENAME': Option('celerybeat-schedule'), | |||||
'SYNC_EVERY': Option(0, type='int'), | |||||
'MAX_LOOP_INTERVAL': Option(0, type='float'), | |||||
'LOG_LEVEL': Option('INFO', deprecate_by='2.4', remove_by='4.0', | |||||
alt='--loglevel argument'), | |||||
'LOG_FILE': Option(deprecate_by='2.4', remove_by='4.0', | |||||
alt='--logfile argument'), | |||||
}, | |||||
'CELERYMON': { | |||||
'LOG_LEVEL': Option('INFO', deprecate_by='2.4', remove_by='4.0', | |||||
alt='--loglevel argument'), | |||||
'LOG_FILE': Option(deprecate_by='2.4', remove_by='4.0', | |||||
alt='--logfile argument'), | |||||
'LOG_FORMAT': Option(DEFAULT_LOG_FMT), | |||||
}, | |||||
'EMAIL': { | |||||
'HOST': Option('localhost'), | |||||
'PORT': Option(25, type='int'), | |||||
'HOST_USER': Option(), | |||||
'HOST_PASSWORD': Option(), | |||||
'TIMEOUT': Option(2, type='float'), | |||||
'USE_SSL': Option(False, type='bool'), | |||||
'USE_TLS': Option(False, type='bool'), | |||||
}, | |||||
'SERVER_EMAIL': Option('celery@localhost'), | |||||
'ADMINS': Option((), type='tuple'), | |||||
} | |||||
def flatten(d, ns=''): | |||||
stack = deque([(ns, d)]) | |||||
while stack: | |||||
name, space = stack.popleft() | |||||
for key, value in items(space): | |||||
if isinstance(value, dict): | |||||
stack.append((name + key + '_', value)) | |||||
else: | |||||
yield name + key, value | |||||
DEFAULTS = dict((key, value.default) for key, value in flatten(NAMESPACES)) | |||||
def find_deprecated_settings(source): | |||||
from celery.utils import warn_deprecated | |||||
for name, opt in flatten(NAMESPACES): | |||||
if (opt.deprecate_by or opt.remove_by) and getattr(source, name, None): | |||||
warn_deprecated(description='The {0!r} setting'.format(name), | |||||
deprecation=opt.deprecate_by, | |||||
removal=opt.remove_by, | |||||
alternative='Use the {0.alt} instead'.format(opt)) | |||||
return source | |||||
@memoize(maxsize=None) | |||||
def find(name, namespace='celery'): | |||||
# - Try specified namespace first. | |||||
namespace = namespace.upper() | |||||
try: | |||||
return searchresult( | |||||
namespace, name.upper(), NAMESPACES[namespace][name.upper()], | |||||
) | |||||
except KeyError: | |||||
# - Try all the other namespaces. | |||||
for ns, keys in items(NAMESPACES): | |||||
if ns.upper() == name.upper(): | |||||
return searchresult(None, ns, keys) | |||||
elif isinstance(keys, dict): | |||||
try: | |||||
return searchresult(ns, name.upper(), keys[name.upper()]) | |||||
except KeyError: | |||||
pass | |||||
# - See if name is a qualname last. | |||||
return searchresult(None, name.upper(), DEFAULTS[name.upper()]) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.log | |||||
~~~~~~~~~~~~~~ | |||||
The Celery instances logging section: ``Celery.log``. | |||||
Sets up logging for the worker and other programs, | |||||
redirects stdouts, colors log output, patches logging | |||||
related compatibility fixes, and so on. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import logging | |||||
import os | |||||
import sys | |||||
from logging.handlers import WatchedFileHandler | |||||
from kombu.log import NullHandler | |||||
from kombu.utils.encoding import set_default_encoding_file | |||||
from celery import signals | |||||
from celery._state import get_current_task | |||||
from celery.five import class_property, string_t | |||||
from celery.utils import isatty, node_format | |||||
from celery.utils.log import ( | |||||
get_logger, mlevel, | |||||
ColorFormatter, ensure_process_aware_logger, | |||||
LoggingProxy, get_multiprocessing_logger, | |||||
reset_multiprocessing_logger, | |||||
) | |||||
from celery.utils.term import colored | |||||
__all__ = ['TaskFormatter', 'Logging'] | |||||
MP_LOG = os.environ.get('MP_LOG', False) | |||||
class TaskFormatter(ColorFormatter): | |||||
def format(self, record): | |||||
task = get_current_task() | |||||
if task and task.request: | |||||
record.__dict__.update(task_id=task.request.id, | |||||
task_name=task.name) | |||||
else: | |||||
record.__dict__.setdefault('task_name', '???') | |||||
record.__dict__.setdefault('task_id', '???') | |||||
return ColorFormatter.format(self, record) | |||||
class Logging(object): | |||||
#: The logging subsystem is only configured once per process. | |||||
#: setup_logging_subsystem sets this flag, and subsequent calls | |||||
#: will do nothing. | |||||
_setup = False | |||||
def __init__(self, app): | |||||
self.app = app | |||||
self.loglevel = mlevel(self.app.conf.CELERYD_LOG_LEVEL) | |||||
self.format = self.app.conf.CELERYD_LOG_FORMAT | |||||
self.task_format = self.app.conf.CELERYD_TASK_LOG_FORMAT | |||||
self.colorize = self.app.conf.CELERYD_LOG_COLOR | |||||
def setup(self, loglevel=None, logfile=None, redirect_stdouts=False, | |||||
redirect_level='WARNING', colorize=None, hostname=None): | |||||
handled = self.setup_logging_subsystem( | |||||
loglevel, logfile, colorize=colorize, hostname=hostname, | |||||
) | |||||
if not handled: | |||||
if redirect_stdouts: | |||||
self.redirect_stdouts(redirect_level) | |||||
os.environ.update( | |||||
CELERY_LOG_LEVEL=str(loglevel) if loglevel else '', | |||||
CELERY_LOG_FILE=str(logfile) if logfile else '', | |||||
) | |||||
return handled | |||||
def redirect_stdouts(self, loglevel=None, name='celery.redirected'): | |||||
self.redirect_stdouts_to_logger( | |||||
get_logger(name), loglevel=loglevel | |||||
) | |||||
os.environ.update( | |||||
CELERY_LOG_REDIRECT='1', | |||||
CELERY_LOG_REDIRECT_LEVEL=str(loglevel or ''), | |||||
) | |||||
def setup_logging_subsystem(self, loglevel=None, logfile=None, format=None, | |||||
colorize=None, hostname=None, **kwargs): | |||||
if self.already_setup: | |||||
return | |||||
if logfile and hostname: | |||||
logfile = node_format(logfile, hostname) | |||||
self.already_setup = True | |||||
loglevel = mlevel(loglevel or self.loglevel) | |||||
format = format or self.format | |||||
colorize = self.supports_color(colorize, logfile) | |||||
reset_multiprocessing_logger() | |||||
ensure_process_aware_logger() | |||||
receivers = signals.setup_logging.send( | |||||
sender=None, loglevel=loglevel, logfile=logfile, | |||||
format=format, colorize=colorize, | |||||
) | |||||
if not receivers: | |||||
root = logging.getLogger() | |||||
if self.app.conf.CELERYD_HIJACK_ROOT_LOGGER: | |||||
root.handlers = [] | |||||
get_logger('celery').handlers = [] | |||||
get_logger('celery.task').handlers = [] | |||||
get_logger('celery.redirected').handlers = [] | |||||
# Configure root logger | |||||
self._configure_logger( | |||||
root, logfile, loglevel, format, colorize, **kwargs | |||||
) | |||||
# Configure the multiprocessing logger | |||||
self._configure_logger( | |||||
get_multiprocessing_logger(), | |||||
logfile, loglevel if MP_LOG else logging.ERROR, | |||||
format, colorize, **kwargs | |||||
) | |||||
signals.after_setup_logger.send( | |||||
sender=None, logger=root, | |||||
loglevel=loglevel, logfile=logfile, | |||||
format=format, colorize=colorize, | |||||
) | |||||
# then setup the root task logger. | |||||
self.setup_task_loggers(loglevel, logfile, colorize=colorize) | |||||
try: | |||||
stream = logging.getLogger().handlers[0].stream | |||||
except (AttributeError, IndexError): | |||||
pass | |||||
else: | |||||
set_default_encoding_file(stream) | |||||
# This is a hack for multiprocessing's fork+exec, so that | |||||
# logging before Process.run works. | |||||
logfile_name = logfile if isinstance(logfile, string_t) else '' | |||||
os.environ.update(_MP_FORK_LOGLEVEL_=str(loglevel), | |||||
_MP_FORK_LOGFILE_=logfile_name, | |||||
_MP_FORK_LOGFORMAT_=format) | |||||
return receivers | |||||
def _configure_logger(self, logger, logfile, loglevel, | |||||
format, colorize, **kwargs): | |||||
if logger is not None: | |||||
self.setup_handlers(logger, logfile, format, | |||||
colorize, **kwargs) | |||||
if loglevel: | |||||
logger.setLevel(loglevel) | |||||
def setup_task_loggers(self, loglevel=None, logfile=None, format=None, | |||||
colorize=None, propagate=False, **kwargs): | |||||
"""Setup the task logger. | |||||
If `logfile` is not specified, then `sys.stderr` is used. | |||||
Will return the base task logger object. | |||||
""" | |||||
loglevel = mlevel(loglevel or self.loglevel) | |||||
format = format or self.task_format | |||||
colorize = self.supports_color(colorize, logfile) | |||||
logger = self.setup_handlers( | |||||
get_logger('celery.task'), | |||||
logfile, format, colorize, | |||||
formatter=TaskFormatter, **kwargs | |||||
) | |||||
logger.setLevel(loglevel) | |||||
# this is an int for some reason, better not question why. | |||||
logger.propagate = int(propagate) | |||||
signals.after_setup_task_logger.send( | |||||
sender=None, logger=logger, | |||||
loglevel=loglevel, logfile=logfile, | |||||
format=format, colorize=colorize, | |||||
) | |||||
return logger | |||||
def redirect_stdouts_to_logger(self, logger, loglevel=None, | |||||
stdout=True, stderr=True): | |||||
"""Redirect :class:`sys.stdout` and :class:`sys.stderr` to a | |||||
logging instance. | |||||
:param logger: The :class:`logging.Logger` instance to redirect to. | |||||
:param loglevel: The loglevel redirected messages will be logged as. | |||||
""" | |||||
proxy = LoggingProxy(logger, loglevel) | |||||
if stdout: | |||||
sys.stdout = proxy | |||||
if stderr: | |||||
sys.stderr = proxy | |||||
return proxy | |||||
def supports_color(self, colorize=None, logfile=None): | |||||
colorize = self.colorize if colorize is None else colorize | |||||
if self.app.IS_WINDOWS: | |||||
# Windows does not support ANSI color codes. | |||||
return False | |||||
if colorize or colorize is None: | |||||
# Only use color if there is no active log file | |||||
# and stderr is an actual terminal. | |||||
return logfile is None and isatty(sys.stderr) | |||||
return colorize | |||||
def colored(self, logfile=None, enabled=None): | |||||
return colored(enabled=self.supports_color(enabled, logfile)) | |||||
def setup_handlers(self, logger, logfile, format, colorize, | |||||
formatter=ColorFormatter, **kwargs): | |||||
if self._is_configured(logger): | |||||
return logger | |||||
handler = self._detect_handler(logfile) | |||||
handler.setFormatter(formatter(format, use_color=colorize)) | |||||
logger.addHandler(handler) | |||||
return logger | |||||
def _detect_handler(self, logfile=None): | |||||
"""Create log handler with either a filename, an open stream | |||||
or :const:`None` (stderr).""" | |||||
logfile = sys.__stderr__ if logfile is None else logfile | |||||
if hasattr(logfile, 'write'): | |||||
return logging.StreamHandler(logfile) | |||||
return WatchedFileHandler(logfile) | |||||
def _has_handler(self, logger): | |||||
if logger.handlers: | |||||
return any(not isinstance(h, NullHandler) for h in logger.handlers) | |||||
def _is_configured(self, logger): | |||||
return self._has_handler(logger) and not getattr( | |||||
logger, '_rudimentary_setup', False) | |||||
def setup_logger(self, name='celery', *args, **kwargs): | |||||
"""Deprecated: No longer used.""" | |||||
self.setup_logging_subsystem(*args, **kwargs) | |||||
return logging.root | |||||
def get_default_logger(self, name='celery', **kwargs): | |||||
return get_logger(name) | |||||
@class_property | |||||
def already_setup(cls): | |||||
return cls._setup | |||||
@already_setup.setter # noqa | |||||
def already_setup(cls, was_setup): | |||||
cls._setup = was_setup |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.registry | |||||
~~~~~~~~~~~~~~~~~~~ | |||||
Registry of available tasks. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import inspect | |||||
from importlib import import_module | |||||
from celery._state import get_current_app | |||||
from celery.exceptions import NotRegistered | |||||
from celery.five import items | |||||
__all__ = ['TaskRegistry'] | |||||
class TaskRegistry(dict): | |||||
NotRegistered = NotRegistered | |||||
def __missing__(self, key): | |||||
raise self.NotRegistered(key) | |||||
def register(self, task): | |||||
"""Register a task in the task registry. | |||||
The task will be automatically instantiated if not already an | |||||
instance. | |||||
""" | |||||
self[task.name] = inspect.isclass(task) and task() or task | |||||
def unregister(self, name): | |||||
"""Unregister task by name. | |||||
:param name: name of the task to unregister, or a | |||||
:class:`celery.task.base.Task` with a valid `name` attribute. | |||||
:raises celery.exceptions.NotRegistered: if the task has not | |||||
been registered. | |||||
""" | |||||
try: | |||||
self.pop(getattr(name, 'name', name)) | |||||
except KeyError: | |||||
raise self.NotRegistered(name) | |||||
# -- these methods are irrelevant now and will be removed in 4.0 | |||||
def regular(self): | |||||
return self.filter_types('regular') | |||||
def periodic(self): | |||||
return self.filter_types('periodic') | |||||
def filter_types(self, type): | |||||
return dict((name, task) for name, task in items(self) | |||||
if getattr(task, 'type', 'regular') == type) | |||||
def _unpickle_task(name): | |||||
return get_current_app().tasks[name] | |||||
def _unpickle_task_v2(name, module=None): | |||||
if module: | |||||
import_module(module) | |||||
return get_current_app().tasks[name] |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.routes | |||||
~~~~~~~~~~~~~ | |||||
Contains utilities for working with task routers, | |||||
(:setting:`CELERY_ROUTES`). | |||||
""" | |||||
from __future__ import absolute_import | |||||
from celery.exceptions import QueueNotFound | |||||
from celery.five import string_t | |||||
from celery.utils import lpmerge | |||||
from celery.utils.functional import firstmethod, mlazy | |||||
from celery.utils.imports import instantiate | |||||
__all__ = ['MapRoute', 'Router', 'prepare'] | |||||
_first_route = firstmethod('route_for_task') | |||||
class MapRoute(object): | |||||
"""Creates a router out of a :class:`dict`.""" | |||||
def __init__(self, map): | |||||
self.map = map | |||||
def route_for_task(self, task, *args, **kwargs): | |||||
try: | |||||
return dict(self.map[task]) | |||||
except KeyError: | |||||
pass | |||||
except ValueError: | |||||
return {'queue': self.map[task]} | |||||
class Router(object): | |||||
def __init__(self, routes=None, queues=None, | |||||
create_missing=False, app=None): | |||||
self.app = app | |||||
self.queues = {} if queues is None else queues | |||||
self.routes = [] if routes is None else routes | |||||
self.create_missing = create_missing | |||||
def route(self, options, task, args=(), kwargs={}): | |||||
options = self.expand_destination(options) # expands 'queue' | |||||
if self.routes: | |||||
route = self.lookup_route(task, args, kwargs) | |||||
if route: # expands 'queue' in route. | |||||
return lpmerge(self.expand_destination(route), options) | |||||
if 'queue' not in options: | |||||
options = lpmerge(self.expand_destination( | |||||
self.app.conf.CELERY_DEFAULT_QUEUE), options) | |||||
return options | |||||
def expand_destination(self, route): | |||||
# Route can be a queue name: convenient for direct exchanges. | |||||
if isinstance(route, string_t): | |||||
queue, route = route, {} | |||||
else: | |||||
# can use defaults from configured queue, but override specific | |||||
# things (like the routing_key): great for topic exchanges. | |||||
queue = route.pop('queue', None) | |||||
if queue: | |||||
try: | |||||
Q = self.queues[queue] # noqa | |||||
except KeyError: | |||||
raise QueueNotFound( | |||||
'Queue {0!r} missing from CELERY_QUEUES'.format(queue)) | |||||
# needs to be declared by publisher | |||||
route['queue'] = Q | |||||
return route | |||||
def lookup_route(self, task, args=None, kwargs=None): | |||||
return _first_route(self.routes, task, args, kwargs) | |||||
def prepare(routes): | |||||
"""Expands the :setting:`CELERY_ROUTES` setting.""" | |||||
def expand_route(route): | |||||
if isinstance(route, dict): | |||||
return MapRoute(route) | |||||
if isinstance(route, string_t): | |||||
return mlazy(instantiate, route) | |||||
return route | |||||
if routes is None: | |||||
return () | |||||
if not isinstance(routes, (list, tuple)): | |||||
routes = (routes, ) | |||||
return [expand_route(route) for route in routes] |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.task | |||||
~~~~~~~~~~~~~~~ | |||||
Task Implementation: Task request context, and the base task class. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import sys | |||||
from billiard.einfo import ExceptionInfo | |||||
from celery import current_app | |||||
from celery import states | |||||
from celery._state import _task_stack | |||||
from celery.canvas import signature | |||||
from celery.exceptions import MaxRetriesExceededError, Reject, Retry | |||||
from celery.five import class_property, items, with_metaclass | |||||
from celery.local import Proxy | |||||
from celery.result import EagerResult | |||||
from celery.utils import gen_task_name, fun_takes_kwargs, uuid, maybe_reraise | |||||
from celery.utils.functional import mattrgetter, maybe_list | |||||
from celery.utils.imports import instantiate | |||||
from celery.utils.mail import ErrorMail | |||||
from .annotations import resolve_all as resolve_all_annotations | |||||
from .registry import _unpickle_task_v2 | |||||
from .utils import appstr | |||||
__all__ = ['Context', 'Task'] | |||||
#: extracts attributes related to publishing a message from an object. | |||||
extract_exec_options = mattrgetter( | |||||
'queue', 'routing_key', 'exchange', 'priority', 'expires', | |||||
'serializer', 'delivery_mode', 'compression', 'time_limit', | |||||
'soft_time_limit', 'immediate', 'mandatory', # imm+man is deprecated | |||||
) | |||||
# We take __repr__ very seriously around here ;) | |||||
R_BOUND_TASK = '<class {0.__name__} of {app}{flags}>' | |||||
R_UNBOUND_TASK = '<unbound {0.__name__}{flags}>' | |||||
R_SELF_TASK = '<@task {0.name} bound to other {0.__self__}>' | |||||
R_INSTANCE = '<@task: {0.name} of {app}{flags}>' | |||||
class _CompatShared(object): | |||||
def __init__(self, name, cons): | |||||
self.name = name | |||||
self.cons = cons | |||||
def __hash__(self): | |||||
return hash(self.name) | |||||
def __repr__(self): | |||||
return '<OldTask: %r>' % (self.name, ) | |||||
def __call__(self, app): | |||||
return self.cons(app) | |||||
def _strflags(flags, default=''): | |||||
if flags: | |||||
return ' ({0})'.format(', '.join(flags)) | |||||
return default | |||||
def _reprtask(task, fmt=None, flags=None): | |||||
flags = list(flags) if flags is not None else [] | |||||
flags.append('v2 compatible') if task.__v2_compat__ else None | |||||
if not fmt: | |||||
fmt = R_BOUND_TASK if task._app else R_UNBOUND_TASK | |||||
return fmt.format( | |||||
task, flags=_strflags(flags), | |||||
app=appstr(task._app) if task._app else None, | |||||
) | |||||
class Context(object): | |||||
# Default context | |||||
logfile = None | |||||
loglevel = None | |||||
hostname = None | |||||
id = None | |||||
args = None | |||||
kwargs = None | |||||
retries = 0 | |||||
eta = None | |||||
expires = None | |||||
is_eager = False | |||||
headers = None | |||||
delivery_info = None | |||||
reply_to = None | |||||
correlation_id = None | |||||
taskset = None # compat alias to group | |||||
group = None | |||||
chord = None | |||||
utc = None | |||||
called_directly = True | |||||
callbacks = None | |||||
errbacks = None | |||||
timelimit = None | |||||
_children = None # see property | |||||
_protected = 0 | |||||
def __init__(self, *args, **kwargs): | |||||
self.update(*args, **kwargs) | |||||
def update(self, *args, **kwargs): | |||||
return self.__dict__.update(*args, **kwargs) | |||||
def clear(self): | |||||
return self.__dict__.clear() | |||||
def get(self, key, default=None): | |||||
return getattr(self, key, default) | |||||
def __repr__(self): | |||||
return '<Context: {0!r}>'.format(vars(self)) | |||||
@property | |||||
def children(self): | |||||
# children must be an empy list for every thread | |||||
if self._children is None: | |||||
self._children = [] | |||||
return self._children | |||||
class TaskType(type): | |||||
"""Meta class for tasks. | |||||
Automatically registers the task in the task registry (except | |||||
if the :attr:`Task.abstract`` attribute is set). | |||||
If no :attr:`Task.name` attribute is provided, then the name is generated | |||||
from the module and class name. | |||||
""" | |||||
_creation_count = {} # used by old non-abstract task classes | |||||
def __new__(cls, name, bases, attrs): | |||||
new = super(TaskType, cls).__new__ | |||||
task_module = attrs.get('__module__') or '__main__' | |||||
# - Abstract class: abstract attribute should not be inherited. | |||||
abstract = attrs.pop('abstract', None) | |||||
if abstract or not attrs.get('autoregister', True): | |||||
return new(cls, name, bases, attrs) | |||||
# The 'app' attribute is now a property, with the real app located | |||||
# in the '_app' attribute. Previously this was a regular attribute, | |||||
# so we should support classes defining it. | |||||
app = attrs.pop('_app', None) or attrs.pop('app', None) | |||||
# Attempt to inherit app from one the bases | |||||
if not isinstance(app, Proxy) and app is None: | |||||
for base in bases: | |||||
if getattr(base, '_app', None): | |||||
app = base._app | |||||
break | |||||
else: | |||||
app = current_app._get_current_object() | |||||
attrs['_app'] = app | |||||
# - Automatically generate missing/empty name. | |||||
task_name = attrs.get('name') | |||||
if not task_name: | |||||
attrs['name'] = task_name = gen_task_name(app, name, task_module) | |||||
if not attrs.get('_decorated'): | |||||
# non decorated tasks must also be shared in case | |||||
# an app is created multiple times due to modules | |||||
# imported under multiple names. | |||||
# Hairy stuff, here to be compatible with 2.x. | |||||
# People should not use non-abstract task classes anymore, | |||||
# use the task decorator. | |||||
from celery._state import connect_on_app_finalize | |||||
unique_name = '.'.join([task_module, name]) | |||||
if unique_name not in cls._creation_count: | |||||
# the creation count is used as a safety | |||||
# so that the same task is not added recursively | |||||
# to the set of constructors. | |||||
cls._creation_count[unique_name] = 1 | |||||
connect_on_app_finalize(_CompatShared( | |||||
unique_name, | |||||
lambda app: TaskType.__new__(cls, name, bases, | |||||
dict(attrs, _app=app)), | |||||
)) | |||||
# - Create and register class. | |||||
# Because of the way import happens (recursively) | |||||
# we may or may not be the first time the task tries to register | |||||
# with the framework. There should only be one class for each task | |||||
# name, so we always return the registered version. | |||||
tasks = app._tasks | |||||
if task_name not in tasks: | |||||
tasks.register(new(cls, name, bases, attrs)) | |||||
instance = tasks[task_name] | |||||
instance.bind(app) | |||||
return instance.__class__ | |||||
def __repr__(cls): | |||||
return _reprtask(cls) | |||||
@with_metaclass(TaskType) | |||||
class Task(object): | |||||
"""Task base class. | |||||
When called tasks apply the :meth:`run` method. This method must | |||||
be defined by all tasks (that is unless the :meth:`__call__` method | |||||
is overridden). | |||||
""" | |||||
__trace__ = None | |||||
__v2_compat__ = False # set by old base in celery.task.base | |||||
ErrorMail = ErrorMail | |||||
MaxRetriesExceededError = MaxRetriesExceededError | |||||
#: Execution strategy used, or the qualified name of one. | |||||
Strategy = 'celery.worker.strategy:default' | |||||
#: This is the instance bound to if the task is a method of a class. | |||||
__self__ = None | |||||
#: The application instance associated with this task class. | |||||
_app = None | |||||
#: Name of the task. | |||||
name = None | |||||
#: If :const:`True` the task is an abstract base class. | |||||
abstract = True | |||||
#: If disabled the worker will not forward magic keyword arguments. | |||||
#: Deprecated and scheduled for removal in v4.0. | |||||
accept_magic_kwargs = False | |||||
#: Maximum number of retries before giving up. If set to :const:`None`, | |||||
#: it will **never** stop retrying. | |||||
max_retries = 3 | |||||
#: Default time in seconds before a retry of the task should be | |||||
#: executed. 3 minutes by default. | |||||
default_retry_delay = 3 * 60 | |||||
#: Rate limit for this task type. Examples: :const:`None` (no rate | |||||
#: limit), `'100/s'` (hundred tasks a second), `'100/m'` (hundred tasks | |||||
#: a minute),`'100/h'` (hundred tasks an hour) | |||||
rate_limit = None | |||||
#: If enabled the worker will not store task state and return values | |||||
#: for this task. Defaults to the :setting:`CELERY_IGNORE_RESULT` | |||||
#: setting. | |||||
ignore_result = None | |||||
#: If enabled the request will keep track of subtasks started by | |||||
#: this task, and this information will be sent with the result | |||||
#: (``result.children``). | |||||
trail = True | |||||
#: If enabled the worker will send monitoring events related to | |||||
#: this task (but only if the worker is configured to send | |||||
#: task related events). | |||||
#: Note that this has no effect on the task-failure event case | |||||
#: where a task is not registered (as it will have no task class | |||||
#: to check this flag). | |||||
send_events = True | |||||
#: When enabled errors will be stored even if the task is otherwise | |||||
#: configured to ignore results. | |||||
store_errors_even_if_ignored = None | |||||
#: If enabled an email will be sent to :setting:`ADMINS` whenever a task | |||||
#: of this type fails. | |||||
send_error_emails = None | |||||
#: The name of a serializer that are registered with | |||||
#: :mod:`kombu.serialization.registry`. Default is `'pickle'`. | |||||
serializer = None | |||||
#: Hard time limit. | |||||
#: Defaults to the :setting:`CELERYD_TASK_TIME_LIMIT` setting. | |||||
time_limit = None | |||||
#: Soft time limit. | |||||
#: Defaults to the :setting:`CELERYD_TASK_SOFT_TIME_LIMIT` setting. | |||||
soft_time_limit = None | |||||
#: The result store backend used for this task. | |||||
backend = None | |||||
#: If disabled this task won't be registered automatically. | |||||
autoregister = True | |||||
#: If enabled the task will report its status as 'started' when the task | |||||
#: is executed by a worker. Disabled by default as the normal behaviour | |||||
#: is to not report that level of granularity. Tasks are either pending, | |||||
#: finished, or waiting to be retried. | |||||
#: | |||||
#: Having a 'started' status can be useful for when there are long | |||||
#: running tasks and there is a need to report which task is currently | |||||
#: running. | |||||
#: | |||||
#: The application default can be overridden using the | |||||
#: :setting:`CELERY_TRACK_STARTED` setting. | |||||
track_started = None | |||||
#: When enabled messages for this task will be acknowledged **after** | |||||
#: the task has been executed, and not *just before* which is the | |||||
#: default behavior. | |||||
#: | |||||
#: Please note that this means the task may be executed twice if the | |||||
#: worker crashes mid execution (which may be acceptable for some | |||||
#: applications). | |||||
#: | |||||
#: The application default can be overridden with the | |||||
#: :setting:`CELERY_ACKS_LATE` setting. | |||||
acks_late = None | |||||
#: Tuple of expected exceptions. | |||||
#: | |||||
#: These are errors that are expected in normal operation | |||||
#: and that should not be regarded as a real error by the worker. | |||||
#: Currently this means that the state will be updated to an error | |||||
#: state, but the worker will not log the event as an error. | |||||
throws = () | |||||
#: Default task expiry time. | |||||
expires = None | |||||
#: Some may expect a request to exist even if the task has not been | |||||
#: called. This should probably be deprecated. | |||||
_default_request = None | |||||
_exec_options = None | |||||
__bound__ = False | |||||
from_config = ( | |||||
('send_error_emails', 'CELERY_SEND_TASK_ERROR_EMAILS'), | |||||
('serializer', 'CELERY_TASK_SERIALIZER'), | |||||
('rate_limit', 'CELERY_DEFAULT_RATE_LIMIT'), | |||||
('track_started', 'CELERY_TRACK_STARTED'), | |||||
('acks_late', 'CELERY_ACKS_LATE'), | |||||
('ignore_result', 'CELERY_IGNORE_RESULT'), | |||||
('store_errors_even_if_ignored', | |||||
'CELERY_STORE_ERRORS_EVEN_IF_IGNORED'), | |||||
) | |||||
_backend = None # set by backend property. | |||||
__bound__ = False | |||||
# - Tasks are lazily bound, so that configuration is not set | |||||
# - until the task is actually used | |||||
@classmethod | |||||
def bind(self, app): | |||||
was_bound, self.__bound__ = self.__bound__, True | |||||
self._app = app | |||||
conf = app.conf | |||||
self._exec_options = None # clear option cache | |||||
for attr_name, config_name in self.from_config: | |||||
if getattr(self, attr_name, None) is None: | |||||
setattr(self, attr_name, conf[config_name]) | |||||
if self.accept_magic_kwargs is None: | |||||
self.accept_magic_kwargs = app.accept_magic_kwargs | |||||
# decorate with annotations from config. | |||||
if not was_bound: | |||||
self.annotate() | |||||
from celery.utils.threads import LocalStack | |||||
self.request_stack = LocalStack() | |||||
# PeriodicTask uses this to add itself to the PeriodicTask schedule. | |||||
self.on_bound(app) | |||||
return app | |||||
@classmethod | |||||
def on_bound(self, app): | |||||
"""This method can be defined to do additional actions when the | |||||
task class is bound to an app.""" | |||||
pass | |||||
@classmethod | |||||
def _get_app(self): | |||||
if self._app is None: | |||||
self._app = current_app | |||||
if not self.__bound__: | |||||
# The app property's __set__ method is not called | |||||
# if Task.app is set (on the class), so must bind on use. | |||||
self.bind(self._app) | |||||
return self._app | |||||
app = class_property(_get_app, bind) | |||||
@classmethod | |||||
def annotate(self): | |||||
for d in resolve_all_annotations(self.app.annotations, self): | |||||
for key, value in items(d): | |||||
if key.startswith('@'): | |||||
self.add_around(key[1:], value) | |||||
else: | |||||
setattr(self, key, value) | |||||
@classmethod | |||||
def add_around(self, attr, around): | |||||
orig = getattr(self, attr) | |||||
if getattr(orig, '__wrapped__', None): | |||||
orig = orig.__wrapped__ | |||||
meth = around(orig) | |||||
meth.__wrapped__ = orig | |||||
setattr(self, attr, meth) | |||||
def __call__(self, *args, **kwargs): | |||||
_task_stack.push(self) | |||||
self.push_request() | |||||
try: | |||||
# add self if this is a bound task | |||||
if self.__self__ is not None: | |||||
return self.run(self.__self__, *args, **kwargs) | |||||
return self.run(*args, **kwargs) | |||||
finally: | |||||
self.pop_request() | |||||
_task_stack.pop() | |||||
def __reduce__(self): | |||||
# - tasks are pickled into the name of the task only, and the reciever | |||||
# - simply grabs it from the local registry. | |||||
# - in later versions the module of the task is also included, | |||||
# - and the receiving side tries to import that module so that | |||||
# - it will work even if the task has not been registered. | |||||
mod = type(self).__module__ | |||||
mod = mod if mod and mod in sys.modules else None | |||||
return (_unpickle_task_v2, (self.name, mod), None) | |||||
def run(self, *args, **kwargs): | |||||
"""The body of the task executed by workers.""" | |||||
raise NotImplementedError('Tasks must define the run method.') | |||||
def start_strategy(self, app, consumer, **kwargs): | |||||
return instantiate(self.Strategy, self, app, consumer, **kwargs) | |||||
def delay(self, *args, **kwargs): | |||||
"""Star argument version of :meth:`apply_async`. | |||||
Does not support the extra options enabled by :meth:`apply_async`. | |||||
:param \*args: positional arguments passed on to the task. | |||||
:param \*\*kwargs: keyword arguments passed on to the task. | |||||
:returns :class:`celery.result.AsyncResult`: | |||||
""" | |||||
return self.apply_async(args, kwargs) | |||||
def apply_async(self, args=None, kwargs=None, task_id=None, producer=None, | |||||
link=None, link_error=None, **options): | |||||
"""Apply tasks asynchronously by sending a message. | |||||
:keyword args: The positional arguments to pass on to the | |||||
task (a :class:`list` or :class:`tuple`). | |||||
:keyword kwargs: The keyword arguments to pass on to the | |||||
task (a :class:`dict`) | |||||
:keyword countdown: Number of seconds into the future that the | |||||
task should execute. Defaults to immediate | |||||
execution. | |||||
:keyword eta: A :class:`~datetime.datetime` object describing | |||||
the absolute time and date of when the task should | |||||
be executed. May not be specified if `countdown` | |||||
is also supplied. | |||||
:keyword expires: Either a :class:`int`, describing the number of | |||||
seconds, or a :class:`~datetime.datetime` object | |||||
that describes the absolute time and date of when | |||||
the task should expire. The task will not be | |||||
executed after the expiration time. | |||||
:keyword connection: Re-use existing broker connection instead | |||||
of establishing a new one. | |||||
:keyword retry: If enabled sending of the task message will be retried | |||||
in the event of connection loss or failure. Default | |||||
is taken from the :setting:`CELERY_TASK_PUBLISH_RETRY` | |||||
setting. Note that you need to handle the | |||||
producer/connection manually for this to work. | |||||
:keyword retry_policy: Override the retry policy used. See the | |||||
:setting:`CELERY_TASK_PUBLISH_RETRY_POLICY` | |||||
setting. | |||||
:keyword routing_key: Custom routing key used to route the task to a | |||||
worker server. If in combination with a | |||||
``queue`` argument only used to specify custom | |||||
routing keys to topic exchanges. | |||||
:keyword queue: The queue to route the task to. This must be a key | |||||
present in :setting:`CELERY_QUEUES`, or | |||||
:setting:`CELERY_CREATE_MISSING_QUEUES` must be | |||||
enabled. See :ref:`guide-routing` for more | |||||
information. | |||||
:keyword exchange: Named custom exchange to send the task to. | |||||
Usually not used in combination with the ``queue`` | |||||
argument. | |||||
:keyword priority: The task priority, a number between 0 and 9. | |||||
Defaults to the :attr:`priority` attribute. | |||||
:keyword serializer: A string identifying the default | |||||
serialization method to use. Can be `pickle`, | |||||
`json`, `yaml`, `msgpack` or any custom | |||||
serialization method that has been registered | |||||
with :mod:`kombu.serialization.registry`. | |||||
Defaults to the :attr:`serializer` attribute. | |||||
:keyword compression: A string identifying the compression method | |||||
to use. Can be one of ``zlib``, ``bzip2``, | |||||
or any custom compression methods registered with | |||||
:func:`kombu.compression.register`. Defaults to | |||||
the :setting:`CELERY_MESSAGE_COMPRESSION` | |||||
setting. | |||||
:keyword link: A single, or a list of tasks to apply if the | |||||
task exits successfully. | |||||
:keyword link_error: A single, or a list of tasks to apply | |||||
if an error occurs while executing the task. | |||||
:keyword producer: :class:~@amqp.TaskProducer` instance to use. | |||||
:keyword add_to_parent: If set to True (default) and the task | |||||
is applied while executing another task, then the result | |||||
will be appended to the parent tasks ``request.children`` | |||||
attribute. Trailing can also be disabled by default using the | |||||
:attr:`trail` attribute | |||||
:keyword publisher: Deprecated alias to ``producer``. | |||||
:keyword headers: Message headers to be sent in the | |||||
task (a :class:`dict`) | |||||
:rtype :class:`celery.result.AsyncResult`: if | |||||
:setting:`CELERY_ALWAYS_EAGER` is not set, otherwise | |||||
:class:`celery.result.EagerResult`. | |||||
Also supports all keyword arguments supported by | |||||
:meth:`kombu.Producer.publish`. | |||||
.. note:: | |||||
If the :setting:`CELERY_ALWAYS_EAGER` setting is set, it will | |||||
be replaced by a local :func:`apply` call instead. | |||||
""" | |||||
app = self._get_app() | |||||
if app.conf.CELERY_ALWAYS_EAGER: | |||||
return self.apply(args, kwargs, task_id=task_id or uuid(), | |||||
link=link, link_error=link_error, **options) | |||||
# add 'self' if this is a "task_method". | |||||
if self.__self__ is not None: | |||||
args = args if isinstance(args, tuple) else tuple(args or ()) | |||||
args = (self.__self__, ) + args | |||||
return app.send_task( | |||||
self.name, args, kwargs, task_id=task_id, producer=producer, | |||||
link=link, link_error=link_error, result_cls=self.AsyncResult, | |||||
**dict(self._get_exec_options(), **options) | |||||
) | |||||
def subtask_from_request(self, request=None, args=None, kwargs=None, | |||||
queue=None, **extra_options): | |||||
request = self.request if request is None else request | |||||
args = request.args if args is None else args | |||||
kwargs = request.kwargs if kwargs is None else kwargs | |||||
limit_hard, limit_soft = request.timelimit or (None, None) | |||||
options = { | |||||
'task_id': request.id, | |||||
'link': request.callbacks, | |||||
'link_error': request.errbacks, | |||||
'group_id': request.group, | |||||
'chord': request.chord, | |||||
'soft_time_limit': limit_soft, | |||||
'time_limit': limit_hard, | |||||
'reply_to': request.reply_to, | |||||
'headers': request.headers, | |||||
} | |||||
options.update( | |||||
{'queue': queue} if queue else (request.delivery_info or {}) | |||||
) | |||||
return self.subtask(args, kwargs, options, type=self, **extra_options) | |||||
def retry(self, args=None, kwargs=None, exc=None, throw=True, | |||||
eta=None, countdown=None, max_retries=None, **options): | |||||
"""Retry the task. | |||||
:param args: Positional arguments to retry with. | |||||
:param kwargs: Keyword arguments to retry with. | |||||
:keyword exc: Custom exception to report when the max restart | |||||
limit has been exceeded (default: | |||||
:exc:`~@MaxRetriesExceededError`). | |||||
If this argument is set and retry is called while | |||||
an exception was raised (``sys.exc_info()`` is set) | |||||
it will attempt to reraise the current exception. | |||||
If no exception was raised it will raise the ``exc`` | |||||
argument provided. | |||||
:keyword countdown: Time in seconds to delay the retry for. | |||||
:keyword eta: Explicit time and date to run the retry at | |||||
(must be a :class:`~datetime.datetime` instance). | |||||
:keyword max_retries: If set, overrides the default retry limit for | |||||
this execution. Changes to this parameter do not propagate to | |||||
subsequent task retry attempts. A value of :const:`None`, means | |||||
"use the default", so if you want infinite retries you would | |||||
have to set the :attr:`max_retries` attribute of the task to | |||||
:const:`None` first. | |||||
:keyword time_limit: If set, overrides the default time limit. | |||||
:keyword soft_time_limit: If set, overrides the default soft | |||||
time limit. | |||||
:keyword \*\*options: Any extra options to pass on to | |||||
meth:`apply_async`. | |||||
:keyword throw: If this is :const:`False`, do not raise the | |||||
:exc:`~@Retry` exception, | |||||
that tells the worker to mark the task as being | |||||
retried. Note that this means the task will be | |||||
marked as failed if the task raises an exception, | |||||
or successful if it returns. | |||||
:raises celery.exceptions.Retry: To tell the worker that | |||||
the task has been re-sent for retry. This always happens, | |||||
unless the `throw` keyword argument has been explicitly set | |||||
to :const:`False`, and is considered normal operation. | |||||
**Example** | |||||
.. code-block:: python | |||||
>>> from imaginary_twitter_lib import Twitter | |||||
>>> from proj.celery import app | |||||
>>> @app.task(bind=True) | |||||
... def tweet(self, auth, message): | |||||
... twitter = Twitter(oauth=auth) | |||||
... try: | |||||
... twitter.post_status_update(message) | |||||
... except twitter.FailWhale as exc: | |||||
... # Retry in 5 minutes. | |||||
... raise self.retry(countdown=60 * 5, exc=exc) | |||||
Although the task will never return above as `retry` raises an | |||||
exception to notify the worker, we use `raise` in front of the retry | |||||
to convey that the rest of the block will not be executed. | |||||
""" | |||||
request = self.request | |||||
retries = request.retries + 1 | |||||
max_retries = self.max_retries if max_retries is None else max_retries | |||||
# Not in worker or emulated by (apply/always_eager), | |||||
# so just raise the original exception. | |||||
if request.called_directly: | |||||
maybe_reraise() # raise orig stack if PyErr_Occurred | |||||
raise exc or Retry('Task can be retried', None) | |||||
if not eta and countdown is None: | |||||
countdown = self.default_retry_delay | |||||
is_eager = request.is_eager | |||||
S = self.subtask_from_request( | |||||
request, args, kwargs, | |||||
countdown=countdown, eta=eta, retries=retries, | |||||
**options | |||||
) | |||||
if max_retries is not None and retries > max_retries: | |||||
if exc: | |||||
# first try to reraise the original exception | |||||
maybe_reraise() | |||||
# or if not in an except block then raise the custom exc. | |||||
raise exc | |||||
raise self.MaxRetriesExceededError( | |||||
"Can't retry {0}[{1}] args:{2} kwargs:{3}".format( | |||||
self.name, request.id, S.args, S.kwargs)) | |||||
ret = Retry(exc=exc, when=eta or countdown) | |||||
if is_eager: | |||||
# if task was executed eagerly using apply(), | |||||
# then the retry must also be executed eagerly. | |||||
S.apply().get() | |||||
return ret | |||||
try: | |||||
S.apply_async() | |||||
except Exception as exc: | |||||
raise Reject(exc, requeue=False) | |||||
if throw: | |||||
raise ret | |||||
return ret | |||||
def apply(self, args=None, kwargs=None, | |||||
link=None, link_error=None, **options): | |||||
"""Execute this task locally, by blocking until the task returns. | |||||
:param args: positional arguments passed on to the task. | |||||
:param kwargs: keyword arguments passed on to the task. | |||||
:keyword throw: Re-raise task exceptions. Defaults to | |||||
the :setting:`CELERY_EAGER_PROPAGATES_EXCEPTIONS` | |||||
setting. | |||||
:rtype :class:`celery.result.EagerResult`: | |||||
""" | |||||
# trace imports Task, so need to import inline. | |||||
from celery.app.trace import eager_trace_task | |||||
app = self._get_app() | |||||
args = args or () | |||||
# add 'self' if this is a bound method. | |||||
if self.__self__ is not None: | |||||
args = (self.__self__, ) + tuple(args) | |||||
kwargs = kwargs or {} | |||||
task_id = options.get('task_id') or uuid() | |||||
retries = options.get('retries', 0) | |||||
throw = app.either('CELERY_EAGER_PROPAGATES_EXCEPTIONS', | |||||
options.pop('throw', None)) | |||||
# Make sure we get the task instance, not class. | |||||
task = app._tasks[self.name] | |||||
request = {'id': task_id, | |||||
'retries': retries, | |||||
'is_eager': True, | |||||
'logfile': options.get('logfile'), | |||||
'loglevel': options.get('loglevel', 0), | |||||
'callbacks': maybe_list(link), | |||||
'errbacks': maybe_list(link_error), | |||||
'headers': options.get('headers'), | |||||
'delivery_info': {'is_eager': True}} | |||||
if self.accept_magic_kwargs: | |||||
default_kwargs = {'task_name': task.name, | |||||
'task_id': task_id, | |||||
'task_retries': retries, | |||||
'task_is_eager': True, | |||||
'logfile': options.get('logfile'), | |||||
'loglevel': options.get('loglevel', 0), | |||||
'delivery_info': {'is_eager': True}} | |||||
supported_keys = fun_takes_kwargs(task.run, default_kwargs) | |||||
extend_with = dict((key, val) | |||||
for key, val in items(default_kwargs) | |||||
if key in supported_keys) | |||||
kwargs.update(extend_with) | |||||
tb = None | |||||
retval, info = eager_trace_task(task, task_id, args, kwargs, | |||||
app=self._get_app(), | |||||
request=request, propagate=throw) | |||||
if isinstance(retval, ExceptionInfo): | |||||
retval, tb = retval.exception, retval.traceback | |||||
state = states.SUCCESS if info is None else info.state | |||||
return EagerResult(task_id, retval, state, traceback=tb) | |||||
def AsyncResult(self, task_id, **kwargs): | |||||
"""Get AsyncResult instance for this kind of task. | |||||
:param task_id: Task id to get result for. | |||||
""" | |||||
return self._get_app().AsyncResult(task_id, backend=self.backend, | |||||
task_name=self.name, **kwargs) | |||||
def subtask(self, args=None, *starargs, **starkwargs): | |||||
"""Return :class:`~celery.signature` object for | |||||
this task, wrapping arguments and execution options | |||||
for a single task invocation.""" | |||||
starkwargs.setdefault('app', self.app) | |||||
return signature(self, args, *starargs, **starkwargs) | |||||
def s(self, *args, **kwargs): | |||||
"""``.s(*a, **k) -> .subtask(a, k)``""" | |||||
return self.subtask(args, kwargs) | |||||
def si(self, *args, **kwargs): | |||||
"""``.si(*a, **k) -> .subtask(a, k, immutable=True)``""" | |||||
return self.subtask(args, kwargs, immutable=True) | |||||
def chunks(self, it, n): | |||||
"""Creates a :class:`~celery.canvas.chunks` task for this task.""" | |||||
from celery import chunks | |||||
return chunks(self.s(), it, n, app=self.app) | |||||
def map(self, it): | |||||
"""Creates a :class:`~celery.canvas.xmap` task from ``it``.""" | |||||
from celery import xmap | |||||
return xmap(self.s(), it, app=self.app) | |||||
def starmap(self, it): | |||||
"""Creates a :class:`~celery.canvas.xstarmap` task from ``it``.""" | |||||
from celery import xstarmap | |||||
return xstarmap(self.s(), it, app=self.app) | |||||
def send_event(self, type_, **fields): | |||||
req = self.request | |||||
with self.app.events.default_dispatcher(hostname=req.hostname) as d: | |||||
return d.send(type_, uuid=req.id, **fields) | |||||
def update_state(self, task_id=None, state=None, meta=None): | |||||
"""Update task state. | |||||
:keyword task_id: Id of the task to update, defaults to the | |||||
id of the current task | |||||
:keyword state: New state (:class:`str`). | |||||
:keyword meta: State metadata (:class:`dict`). | |||||
""" | |||||
if task_id is None: | |||||
task_id = self.request.id | |||||
self.backend.store_result(task_id, meta, state) | |||||
def on_success(self, retval, task_id, args, kwargs): | |||||
"""Success handler. | |||||
Run by the worker if the task executes successfully. | |||||
:param retval: The return value of the task. | |||||
:param task_id: Unique id of the executed task. | |||||
:param args: Original arguments for the executed task. | |||||
:param kwargs: Original keyword arguments for the executed task. | |||||
The return value of this handler is ignored. | |||||
""" | |||||
pass | |||||
def on_retry(self, exc, task_id, args, kwargs, einfo): | |||||
"""Retry handler. | |||||
This is run by the worker when the task is to be retried. | |||||
:param exc: The exception sent to :meth:`retry`. | |||||
:param task_id: Unique id of the retried task. | |||||
:param args: Original arguments for the retried task. | |||||
:param kwargs: Original keyword arguments for the retried task. | |||||
:keyword einfo: :class:`~billiard.einfo.ExceptionInfo` | |||||
instance, containing the traceback. | |||||
The return value of this handler is ignored. | |||||
""" | |||||
pass | |||||
def on_failure(self, exc, task_id, args, kwargs, einfo): | |||||
"""Error handler. | |||||
This is run by the worker when the task fails. | |||||
:param exc: The exception raised by the task. | |||||
:param task_id: Unique id of the failed task. | |||||
:param args: Original arguments for the task that failed. | |||||
:param kwargs: Original keyword arguments for the task | |||||
that failed. | |||||
:keyword einfo: :class:`~billiard.einfo.ExceptionInfo` | |||||
instance, containing the traceback. | |||||
The return value of this handler is ignored. | |||||
""" | |||||
pass | |||||
def after_return(self, status, retval, task_id, args, kwargs, einfo): | |||||
"""Handler called after the task returns. | |||||
:param status: Current task state. | |||||
:param retval: Task return value/exception. | |||||
:param task_id: Unique id of the task. | |||||
:param args: Original arguments for the task. | |||||
:param kwargs: Original keyword arguments for the task. | |||||
:keyword einfo: :class:`~billiard.einfo.ExceptionInfo` | |||||
instance, containing the traceback (if any). | |||||
The return value of this handler is ignored. | |||||
""" | |||||
pass | |||||
def send_error_email(self, context, exc, **kwargs): | |||||
if self.send_error_emails and \ | |||||
not getattr(self, 'disable_error_emails', None): | |||||
self.ErrorMail(self, **kwargs).send(context, exc) | |||||
def add_trail(self, result): | |||||
if self.trail: | |||||
self.request.children.append(result) | |||||
return result | |||||
def push_request(self, *args, **kwargs): | |||||
self.request_stack.push(Context(*args, **kwargs)) | |||||
def pop_request(self): | |||||
self.request_stack.pop() | |||||
def __repr__(self): | |||||
"""`repr(task)`""" | |||||
return _reprtask(self, R_SELF_TASK if self.__self__ else R_INSTANCE) | |||||
def _get_request(self): | |||||
"""Get current request object.""" | |||||
req = self.request_stack.top | |||||
if req is None: | |||||
# task was not called, but some may still expect a request | |||||
# to be there, perhaps that should be deprecated. | |||||
if self._default_request is None: | |||||
self._default_request = Context() | |||||
return self._default_request | |||||
return req | |||||
request = property(_get_request) | |||||
def _get_exec_options(self): | |||||
if self._exec_options is None: | |||||
self._exec_options = extract_exec_options(self) | |||||
return self._exec_options | |||||
@property | |||||
def backend(self): | |||||
backend = self._backend | |||||
if backend is None: | |||||
return self.app.backend | |||||
return backend | |||||
@backend.setter | |||||
def backend(self, value): # noqa | |||||
self._backend = value | |||||
@property | |||||
def __name__(self): | |||||
return self.__class__.__name__ | |||||
BaseTask = Task # compat alias |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.trace | |||||
~~~~~~~~~~~~~~~~ | |||||
This module defines how the task execution is traced: | |||||
errors are recorded, handlers are applied and so on. | |||||
""" | |||||
from __future__ import absolute_import | |||||
# ## --- | |||||
# This is the heart of the worker, the inner loop so to speak. | |||||
# It used to be split up into nice little classes and methods, | |||||
# but in the end it only resulted in bad performance and horrible tracebacks, | |||||
# so instead we now use one closure per task class. | |||||
import os | |||||
import socket | |||||
import sys | |||||
from warnings import warn | |||||
from billiard.einfo import ExceptionInfo | |||||
from kombu.exceptions import EncodeError | |||||
from kombu.utils import kwdict | |||||
from celery import current_app, group | |||||
from celery import states, signals | |||||
from celery._state import _task_stack | |||||
from celery.app import set_default_app | |||||
from celery.app.task import Task as BaseTask, Context | |||||
from celery.exceptions import Ignore, Reject, Retry | |||||
from celery.utils.log import get_logger | |||||
from celery.utils.objects import mro_lookup | |||||
from celery.utils.serialization import ( | |||||
get_pickleable_exception, | |||||
get_pickleable_etype, | |||||
) | |||||
__all__ = ['TraceInfo', 'build_tracer', 'trace_task', 'eager_trace_task', | |||||
'setup_worker_optimizations', 'reset_worker_optimizations'] | |||||
_logger = get_logger(__name__) | |||||
send_prerun = signals.task_prerun.send | |||||
send_postrun = signals.task_postrun.send | |||||
send_success = signals.task_success.send | |||||
STARTED = states.STARTED | |||||
SUCCESS = states.SUCCESS | |||||
IGNORED = states.IGNORED | |||||
REJECTED = states.REJECTED | |||||
RETRY = states.RETRY | |||||
FAILURE = states.FAILURE | |||||
EXCEPTION_STATES = states.EXCEPTION_STATES | |||||
IGNORE_STATES = frozenset([IGNORED, RETRY, REJECTED]) | |||||
#: set by :func:`setup_worker_optimizations` | |||||
_tasks = None | |||||
_patched = {} | |||||
def task_has_custom(task, attr): | |||||
"""Return true if the task or one of its bases | |||||
defines ``attr`` (excluding the one in BaseTask).""" | |||||
return mro_lookup(task.__class__, attr, stop=(BaseTask, object), | |||||
monkey_patched=['celery.app.task']) | |||||
class TraceInfo(object): | |||||
__slots__ = ('state', 'retval') | |||||
def __init__(self, state, retval=None): | |||||
self.state = state | |||||
self.retval = retval | |||||
def handle_error_state(self, task, eager=False): | |||||
store_errors = not eager | |||||
if task.ignore_result: | |||||
store_errors = task.store_errors_even_if_ignored | |||||
return { | |||||
RETRY: self.handle_retry, | |||||
FAILURE: self.handle_failure, | |||||
}[self.state](task, store_errors=store_errors) | |||||
def handle_retry(self, task, store_errors=True): | |||||
"""Handle retry exception.""" | |||||
# the exception raised is the Retry semi-predicate, | |||||
# and it's exc' attribute is the original exception raised (if any). | |||||
req = task.request | |||||
type_, _, tb = sys.exc_info() | |||||
try: | |||||
reason = self.retval | |||||
einfo = ExceptionInfo((type_, reason, tb)) | |||||
if store_errors: | |||||
task.backend.mark_as_retry( | |||||
req.id, reason.exc, einfo.traceback, request=req, | |||||
) | |||||
task.on_retry(reason.exc, req.id, req.args, req.kwargs, einfo) | |||||
signals.task_retry.send(sender=task, request=req, | |||||
reason=reason, einfo=einfo) | |||||
return einfo | |||||
finally: | |||||
del(tb) | |||||
def handle_failure(self, task, store_errors=True): | |||||
"""Handle exception.""" | |||||
req = task.request | |||||
type_, _, tb = sys.exc_info() | |||||
try: | |||||
exc = self.retval | |||||
einfo = ExceptionInfo() | |||||
einfo.exception = get_pickleable_exception(einfo.exception) | |||||
einfo.type = get_pickleable_etype(einfo.type) | |||||
if store_errors: | |||||
task.backend.mark_as_failure( | |||||
req.id, exc, einfo.traceback, request=req, | |||||
) | |||||
task.on_failure(exc, req.id, req.args, req.kwargs, einfo) | |||||
signals.task_failure.send(sender=task, task_id=req.id, | |||||
exception=exc, args=req.args, | |||||
kwargs=req.kwargs, | |||||
traceback=tb, | |||||
einfo=einfo) | |||||
return einfo | |||||
finally: | |||||
del(tb) | |||||
def build_tracer(name, task, loader=None, hostname=None, store_errors=True, | |||||
Info=TraceInfo, eager=False, propagate=False, app=None, | |||||
IGNORE_STATES=IGNORE_STATES): | |||||
"""Return a function that traces task execution; catches all | |||||
exceptions and updates result backend with the state and result | |||||
If the call was successful, it saves the result to the task result | |||||
backend, and sets the task status to `"SUCCESS"`. | |||||
If the call raises :exc:`~@Retry`, it extracts | |||||
the original exception, uses that as the result and sets the task state | |||||
to `"RETRY"`. | |||||
If the call results in an exception, it saves the exception as the task | |||||
result, and sets the task state to `"FAILURE"`. | |||||
Return a function that takes the following arguments: | |||||
:param uuid: The id of the task. | |||||
:param args: List of positional args to pass on to the function. | |||||
:param kwargs: Keyword arguments mapping to pass on to the function. | |||||
:keyword request: Request dict. | |||||
""" | |||||
# If the task doesn't define a custom __call__ method | |||||
# we optimize it away by simply calling the run method directly, | |||||
# saving the extra method call and a line less in the stack trace. | |||||
fun = task if task_has_custom(task, '__call__') else task.run | |||||
loader = loader or app.loader | |||||
backend = task.backend | |||||
ignore_result = task.ignore_result | |||||
track_started = task.track_started | |||||
track_started = not eager and (task.track_started and not ignore_result) | |||||
publish_result = not eager and not ignore_result | |||||
hostname = hostname or socket.gethostname() | |||||
loader_task_init = loader.on_task_init | |||||
loader_cleanup = loader.on_process_cleanup | |||||
task_on_success = None | |||||
task_after_return = None | |||||
if task_has_custom(task, 'on_success'): | |||||
task_on_success = task.on_success | |||||
if task_has_custom(task, 'after_return'): | |||||
task_after_return = task.after_return | |||||
store_result = backend.store_result | |||||
backend_cleanup = backend.process_cleanup | |||||
pid = os.getpid() | |||||
request_stack = task.request_stack | |||||
push_request = request_stack.push | |||||
pop_request = request_stack.pop | |||||
push_task = _task_stack.push | |||||
pop_task = _task_stack.pop | |||||
on_chord_part_return = backend.on_chord_part_return | |||||
prerun_receivers = signals.task_prerun.receivers | |||||
postrun_receivers = signals.task_postrun.receivers | |||||
success_receivers = signals.task_success.receivers | |||||
from celery import canvas | |||||
signature = canvas.maybe_signature # maybe_ does not clone if already | |||||
def on_error(request, exc, uuid, state=FAILURE, call_errbacks=True): | |||||
if propagate: | |||||
raise | |||||
I = Info(state, exc) | |||||
R = I.handle_error_state(task, eager=eager) | |||||
if call_errbacks: | |||||
group( | |||||
[signature(errback, app=app) | |||||
for errback in request.errbacks or []], app=app, | |||||
).apply_async((uuid, )) | |||||
return I, R, I.state, I.retval | |||||
def trace_task(uuid, args, kwargs, request=None): | |||||
# R - is the possibly prepared return value. | |||||
# I - is the Info object. | |||||
# retval - is the always unmodified return value. | |||||
# state - is the resulting task state. | |||||
# This function is very long because we have unrolled all the calls | |||||
# for performance reasons, and because the function is so long | |||||
# we want the main variables (I, and R) to stand out visually from the | |||||
# the rest of the variables, so breaking PEP8 is worth it ;) | |||||
R = I = retval = state = None | |||||
kwargs = kwdict(kwargs) | |||||
try: | |||||
push_task(task) | |||||
task_request = Context(request or {}, args=args, | |||||
called_directly=False, kwargs=kwargs) | |||||
push_request(task_request) | |||||
try: | |||||
# -*- PRE -*- | |||||
if prerun_receivers: | |||||
send_prerun(sender=task, task_id=uuid, task=task, | |||||
args=args, kwargs=kwargs) | |||||
loader_task_init(uuid, task) | |||||
if track_started: | |||||
store_result( | |||||
uuid, {'pid': pid, 'hostname': hostname}, STARTED, | |||||
request=task_request, | |||||
) | |||||
# -*- TRACE -*- | |||||
try: | |||||
R = retval = fun(*args, **kwargs) | |||||
state = SUCCESS | |||||
except Reject as exc: | |||||
I, R = Info(REJECTED, exc), ExceptionInfo(internal=True) | |||||
state, retval = I.state, I.retval | |||||
except Ignore as exc: | |||||
I, R = Info(IGNORED, exc), ExceptionInfo(internal=True) | |||||
state, retval = I.state, I.retval | |||||
except Retry as exc: | |||||
I, R, state, retval = on_error( | |||||
task_request, exc, uuid, RETRY, call_errbacks=False, | |||||
) | |||||
except Exception as exc: | |||||
I, R, state, retval = on_error(task_request, exc, uuid) | |||||
except BaseException as exc: | |||||
raise | |||||
else: | |||||
try: | |||||
# callback tasks must be applied before the result is | |||||
# stored, so that result.children is populated. | |||||
# groups are called inline and will store trail | |||||
# separately, so need to call them separately | |||||
# so that the trail's not added multiple times :( | |||||
# (Issue #1936) | |||||
callbacks = task.request.callbacks | |||||
if callbacks: | |||||
if len(task.request.callbacks) > 1: | |||||
sigs, groups = [], [] | |||||
for sig in callbacks: | |||||
sig = signature(sig, app=app) | |||||
if isinstance(sig, group): | |||||
groups.append(sig) | |||||
else: | |||||
sigs.append(sig) | |||||
for group_ in groups: | |||||
group_.apply_async((retval, )) | |||||
if sigs: | |||||
group(sigs).apply_async((retval, )) | |||||
else: | |||||
signature(callbacks[0], app=app).delay(retval) | |||||
if publish_result: | |||||
store_result( | |||||
uuid, retval, SUCCESS, request=task_request, | |||||
) | |||||
except EncodeError as exc: | |||||
I, R, state, retval = on_error(task_request, exc, uuid) | |||||
else: | |||||
if task_on_success: | |||||
task_on_success(retval, uuid, args, kwargs) | |||||
if success_receivers: | |||||
send_success(sender=task, result=retval) | |||||
# -* POST *- | |||||
if state not in IGNORE_STATES: | |||||
if task_request.chord: | |||||
on_chord_part_return(task, state, R) | |||||
if task_after_return: | |||||
task_after_return( | |||||
state, retval, uuid, args, kwargs, None, | |||||
) | |||||
finally: | |||||
try: | |||||
if postrun_receivers: | |||||
send_postrun(sender=task, task_id=uuid, task=task, | |||||
args=args, kwargs=kwargs, | |||||
retval=retval, state=state) | |||||
finally: | |||||
pop_task() | |||||
pop_request() | |||||
if not eager: | |||||
try: | |||||
backend_cleanup() | |||||
loader_cleanup() | |||||
except (KeyboardInterrupt, SystemExit, MemoryError): | |||||
raise | |||||
except Exception as exc: | |||||
_logger.error('Process cleanup failed: %r', exc, | |||||
exc_info=True) | |||||
except MemoryError: | |||||
raise | |||||
except Exception as exc: | |||||
if eager: | |||||
raise | |||||
R = report_internal_error(task, exc) | |||||
return R, I | |||||
return trace_task | |||||
def trace_task(task, uuid, args, kwargs, request={}, **opts): | |||||
try: | |||||
if task.__trace__ is None: | |||||
task.__trace__ = build_tracer(task.name, task, **opts) | |||||
return task.__trace__(uuid, args, kwargs, request)[0] | |||||
except Exception as exc: | |||||
return report_internal_error(task, exc) | |||||
def _trace_task_ret(name, uuid, args, kwargs, request={}, app=None, **opts): | |||||
app = app or current_app | |||||
return trace_task(app.tasks[name], | |||||
uuid, args, kwargs, request, app=app, **opts) | |||||
trace_task_ret = _trace_task_ret | |||||
def _fast_trace_task(task, uuid, args, kwargs, request={}): | |||||
# setup_worker_optimizations will point trace_task_ret to here, | |||||
# so this is the function used in the worker. | |||||
return _tasks[task].__trace__(uuid, args, kwargs, request)[0] | |||||
def eager_trace_task(task, uuid, args, kwargs, request=None, **opts): | |||||
opts.setdefault('eager', True) | |||||
return build_tracer(task.name, task, **opts)( | |||||
uuid, args, kwargs, request) | |||||
def report_internal_error(task, exc): | |||||
_type, _value, _tb = sys.exc_info() | |||||
try: | |||||
_value = task.backend.prepare_exception(exc, 'pickle') | |||||
exc_info = ExceptionInfo((_type, _value, _tb), internal=True) | |||||
warn(RuntimeWarning( | |||||
'Exception raised outside body: {0!r}:\n{1}'.format( | |||||
exc, exc_info.traceback))) | |||||
return exc_info | |||||
finally: | |||||
del(_tb) | |||||
def setup_worker_optimizations(app): | |||||
global _tasks | |||||
global trace_task_ret | |||||
# make sure custom Task.__call__ methods that calls super | |||||
# will not mess up the request/task stack. | |||||
_install_stack_protection() | |||||
# all new threads start without a current app, so if an app is not | |||||
# passed on to the thread it will fall back to the "default app", | |||||
# which then could be the wrong app. So for the worker | |||||
# we set this to always return our app. This is a hack, | |||||
# and means that only a single app can be used for workers | |||||
# running in the same process. | |||||
app.set_current() | |||||
set_default_app(app) | |||||
# evaluate all task classes by finalizing the app. | |||||
app.finalize() | |||||
# set fast shortcut to task registry | |||||
_tasks = app._tasks | |||||
trace_task_ret = _fast_trace_task | |||||
from celery.worker import job as job_module | |||||
job_module.trace_task_ret = _fast_trace_task | |||||
job_module.__optimize__() | |||||
def reset_worker_optimizations(): | |||||
global trace_task_ret | |||||
trace_task_ret = _trace_task_ret | |||||
try: | |||||
delattr(BaseTask, '_stackprotected') | |||||
except AttributeError: | |||||
pass | |||||
try: | |||||
BaseTask.__call__ = _patched.pop('BaseTask.__call__') | |||||
except KeyError: | |||||
pass | |||||
from celery.worker import job as job_module | |||||
job_module.trace_task_ret = _trace_task_ret | |||||
def _install_stack_protection(): | |||||
# Patches BaseTask.__call__ in the worker to handle the edge case | |||||
# where people override it and also call super. | |||||
# | |||||
# - The worker optimizes away BaseTask.__call__ and instead | |||||
# calls task.run directly. | |||||
# - so with the addition of current_task and the request stack | |||||
# BaseTask.__call__ now pushes to those stacks so that | |||||
# they work when tasks are called directly. | |||||
# | |||||
# The worker only optimizes away __call__ in the case | |||||
# where it has not been overridden, so the request/task stack | |||||
# will blow if a custom task class defines __call__ and also | |||||
# calls super(). | |||||
if not getattr(BaseTask, '_stackprotected', False): | |||||
_patched['BaseTask.__call__'] = orig = BaseTask.__call__ | |||||
def __protected_call__(self, *args, **kwargs): | |||||
stack = self.request_stack | |||||
req = stack.top | |||||
if req and not req._protected and \ | |||||
len(stack) == 1 and not req.called_directly: | |||||
req._protected = 1 | |||||
return self.run(*args, **kwargs) | |||||
return orig(self, *args, **kwargs) | |||||
BaseTask.__call__ = __protected_call__ | |||||
BaseTask._stackprotected = True |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.app.utils | |||||
~~~~~~~~~~~~~~~~ | |||||
App utilities: Compat settings, bugreport tool, pickling apps. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import os | |||||
import platform as _platform | |||||
import re | |||||
from collections import Mapping | |||||
from types import ModuleType | |||||
from kombu.utils.url import maybe_sanitize_url | |||||
from celery.datastructures import ConfigurationView | |||||
from celery.five import items, string_t, values | |||||
from celery.platforms import pyimplementation | |||||
from celery.utils.text import pretty | |||||
from celery.utils.imports import import_from_cwd, symbol_by_name, qualname | |||||
from .defaults import find | |||||
__all__ = ['Settings', 'appstr', 'bugreport', | |||||
'filter_hidden_settings', 'find_app'] | |||||
#: Format used to generate bugreport information. | |||||
BUGREPORT_INFO = """ | |||||
software -> celery:{celery_v} kombu:{kombu_v} py:{py_v} | |||||
billiard:{billiard_v} {driver_v} | |||||
platform -> system:{system} arch:{arch} imp:{py_i} | |||||
loader -> {loader} | |||||
settings -> transport:{transport} results:{results} | |||||
{human_settings} | |||||
""" | |||||
HIDDEN_SETTINGS = re.compile( | |||||
'API|TOKEN|KEY|SECRET|PASS|PROFANITIES_LIST|SIGNATURE|DATABASE', | |||||
re.IGNORECASE, | |||||
) | |||||
def appstr(app): | |||||
"""String used in __repr__ etc, to id app instances.""" | |||||
return '{0}:0x{1:x}'.format(app.main or '__main__', id(app)) | |||||
class Settings(ConfigurationView): | |||||
"""Celery settings object. | |||||
.. seealso: | |||||
:ref:`configuration` for a full list of configuration keys. | |||||
""" | |||||
@property | |||||
def CELERY_RESULT_BACKEND(self): | |||||
return self.first('CELERY_RESULT_BACKEND', 'CELERY_BACKEND') | |||||
@property | |||||
def BROKER_TRANSPORT(self): | |||||
return self.first('BROKER_TRANSPORT', | |||||
'BROKER_BACKEND', 'CARROT_BACKEND') | |||||
@property | |||||
def BROKER_BACKEND(self): | |||||
"""Deprecated compat alias to :attr:`BROKER_TRANSPORT`.""" | |||||
return self.BROKER_TRANSPORT | |||||
@property | |||||
def BROKER_URL(self): | |||||
return (os.environ.get('CELERY_BROKER_URL') or | |||||
self.first('BROKER_URL', 'BROKER_HOST')) | |||||
@property | |||||
def CELERY_TIMEZONE(self): | |||||
# this way we also support django's time zone. | |||||
return self.first('CELERY_TIMEZONE', 'TIME_ZONE') | |||||
def without_defaults(self): | |||||
"""Return the current configuration, but without defaults.""" | |||||
# the last stash is the default settings, so just skip that | |||||
return Settings({}, self._order[:-1]) | |||||
def value_set_for(self, key): | |||||
return key in self.without_defaults() | |||||
def find_option(self, name, namespace='celery'): | |||||
"""Search for option by name. | |||||
Will return ``(namespace, key, type)`` tuple, e.g.:: | |||||
>>> from proj.celery import app | |||||
>>> app.conf.find_option('disable_rate_limits') | |||||
('CELERY', 'DISABLE_RATE_LIMITS', | |||||
<Option: type->bool default->False>)) | |||||
:param name: Name of option, cannot be partial. | |||||
:keyword namespace: Preferred namespace (``CELERY`` by default). | |||||
""" | |||||
return find(name, namespace) | |||||
def find_value_for_key(self, name, namespace='celery'): | |||||
"""Shortcut to ``get_by_parts(*find_option(name)[:-1])``""" | |||||
return self.get_by_parts(*self.find_option(name, namespace)[:-1]) | |||||
def get_by_parts(self, *parts): | |||||
"""Return the current value for setting specified as a path. | |||||
Example:: | |||||
>>> from proj.celery import app | |||||
>>> app.conf.get_by_parts('CELERY', 'DISABLE_RATE_LIMITS') | |||||
False | |||||
""" | |||||
return self['_'.join(part for part in parts if part)] | |||||
def table(self, with_defaults=False, censored=True): | |||||
filt = filter_hidden_settings if censored else lambda v: v | |||||
return filt(dict( | |||||
(k, v) for k, v in items( | |||||
self if with_defaults else self.without_defaults()) | |||||
if k.isupper() and not k.startswith('_') | |||||
)) | |||||
def humanize(self, with_defaults=False, censored=True): | |||||
"""Return a human readable string showing changes to the | |||||
configuration.""" | |||||
return '\n'.join( | |||||
'{0}: {1}'.format(key, pretty(value, width=50)) | |||||
for key, value in items(self.table(with_defaults, censored))) | |||||
class AppPickler(object): | |||||
"""Old application pickler/unpickler (< 3.1).""" | |||||
def __call__(self, cls, *args): | |||||
kwargs = self.build_kwargs(*args) | |||||
app = self.construct(cls, **kwargs) | |||||
self.prepare(app, **kwargs) | |||||
return app | |||||
def prepare(self, app, **kwargs): | |||||
app.conf.update(kwargs['changes']) | |||||
def build_kwargs(self, *args): | |||||
return self.build_standard_kwargs(*args) | |||||
def build_standard_kwargs(self, main, changes, loader, backend, amqp, | |||||
events, log, control, accept_magic_kwargs, | |||||
config_source=None): | |||||
return dict(main=main, loader=loader, backend=backend, amqp=amqp, | |||||
changes=changes, events=events, log=log, control=control, | |||||
set_as_current=False, | |||||
accept_magic_kwargs=accept_magic_kwargs, | |||||
config_source=config_source) | |||||
def construct(self, cls, **kwargs): | |||||
return cls(**kwargs) | |||||
def _unpickle_app(cls, pickler, *args): | |||||
"""Rebuild app for versions 2.5+""" | |||||
return pickler()(cls, *args) | |||||
def _unpickle_app_v2(cls, kwargs): | |||||
"""Rebuild app for versions 3.1+""" | |||||
kwargs['set_as_current'] = False | |||||
return cls(**kwargs) | |||||
def filter_hidden_settings(conf): | |||||
def maybe_censor(key, value, mask='*' * 8): | |||||
if isinstance(value, Mapping): | |||||
return filter_hidden_settings(value) | |||||
if isinstance(key, string_t): | |||||
if HIDDEN_SETTINGS.search(key): | |||||
return mask | |||||
elif 'BROKER_URL' in key.upper(): | |||||
from kombu import Connection | |||||
return Connection(value).as_uri(mask=mask) | |||||
elif key.upper() in ('CELERY_RESULT_BACKEND', 'CELERY_BACKEND'): | |||||
return maybe_sanitize_url(value, mask=mask) | |||||
return value | |||||
return dict((k, maybe_censor(k, v)) for k, v in items(conf)) | |||||
def bugreport(app): | |||||
"""Return a string containing information useful in bug reports.""" | |||||
import billiard | |||||
import celery | |||||
import kombu | |||||
try: | |||||
conn = app.connection() | |||||
driver_v = '{0}:{1}'.format(conn.transport.driver_name, | |||||
conn.transport.driver_version()) | |||||
transport = conn.transport_cls | |||||
except Exception: | |||||
transport = driver_v = '' | |||||
return BUGREPORT_INFO.format( | |||||
system=_platform.system(), | |||||
arch=', '.join(x for x in _platform.architecture() if x), | |||||
py_i=pyimplementation(), | |||||
celery_v=celery.VERSION_BANNER, | |||||
kombu_v=kombu.__version__, | |||||
billiard_v=billiard.__version__, | |||||
py_v=_platform.python_version(), | |||||
driver_v=driver_v, | |||||
transport=transport, | |||||
results=maybe_sanitize_url( | |||||
app.conf.CELERY_RESULT_BACKEND or 'disabled'), | |||||
human_settings=app.conf.humanize(), | |||||
loader=qualname(app.loader.__class__), | |||||
) | |||||
def find_app(app, symbol_by_name=symbol_by_name, imp=import_from_cwd): | |||||
from .base import Celery | |||||
try: | |||||
sym = symbol_by_name(app, imp=imp) | |||||
except AttributeError: | |||||
# last part was not an attribute, but a module | |||||
sym = imp(app) | |||||
if isinstance(sym, ModuleType) and ':' not in app: | |||||
try: | |||||
found = sym.app | |||||
if isinstance(found, ModuleType): | |||||
raise AttributeError() | |||||
except AttributeError: | |||||
try: | |||||
found = sym.celery | |||||
if isinstance(found, ModuleType): | |||||
raise AttributeError() | |||||
except AttributeError: | |||||
if getattr(sym, '__path__', None): | |||||
try: | |||||
return find_app( | |||||
'{0}.celery'.format(app), | |||||
symbol_by_name=symbol_by_name, imp=imp, | |||||
) | |||||
except ImportError: | |||||
pass | |||||
for suspect in values(vars(sym)): | |||||
if isinstance(suspect, Celery): | |||||
return suspect | |||||
raise | |||||
else: | |||||
return found | |||||
else: | |||||
return found | |||||
return sym |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.apps.beat | |||||
~~~~~~~~~~~~~~~~ | |||||
This module is the 'program-version' of :mod:`celery.beat`. | |||||
It does everything necessary to run that module | |||||
as an actual application, like installing signal handlers | |||||
and so on. | |||||
""" | |||||
from __future__ import absolute_import, unicode_literals | |||||
import numbers | |||||
import socket | |||||
import sys | |||||
from celery import VERSION_BANNER, platforms, beat | |||||
from celery.utils.imports import qualname | |||||
from celery.utils.log import LOG_LEVELS, get_logger | |||||
from celery.utils.timeutils import humanize_seconds | |||||
__all__ = ['Beat'] | |||||
STARTUP_INFO_FMT = """ | |||||
Configuration -> | |||||
. broker -> {conninfo} | |||||
. loader -> {loader} | |||||
. scheduler -> {scheduler} | |||||
{scheduler_info} | |||||
. logfile -> {logfile}@%{loglevel} | |||||
. maxinterval -> {hmax_interval} ({max_interval}s) | |||||
""".strip() | |||||
logger = get_logger('celery.beat') | |||||
class Beat(object): | |||||
Service = beat.Service | |||||
app = None | |||||
def __init__(self, max_interval=None, app=None, | |||||
socket_timeout=30, pidfile=None, no_color=None, | |||||
loglevel=None, logfile=None, schedule=None, | |||||
scheduler_cls=None, redirect_stdouts=None, | |||||
redirect_stdouts_level=None, **kwargs): | |||||
"""Starts the beat task scheduler.""" | |||||
self.app = app = app or self.app | |||||
self.loglevel = self._getopt('log_level', loglevel) | |||||
self.logfile = self._getopt('log_file', logfile) | |||||
self.schedule = self._getopt('schedule_filename', schedule) | |||||
self.scheduler_cls = self._getopt('scheduler', scheduler_cls) | |||||
self.redirect_stdouts = self._getopt( | |||||
'redirect_stdouts', redirect_stdouts, | |||||
) | |||||
self.redirect_stdouts_level = self._getopt( | |||||
'redirect_stdouts_level', redirect_stdouts_level, | |||||
) | |||||
self.max_interval = max_interval | |||||
self.socket_timeout = socket_timeout | |||||
self.no_color = no_color | |||||
self.colored = app.log.colored( | |||||
self.logfile, | |||||
enabled=not no_color if no_color is not None else no_color, | |||||
) | |||||
self.pidfile = pidfile | |||||
if not isinstance(self.loglevel, numbers.Integral): | |||||
self.loglevel = LOG_LEVELS[self.loglevel.upper()] | |||||
def _getopt(self, key, value): | |||||
if value is not None: | |||||
return value | |||||
return self.app.conf.find_value_for_key(key, namespace='celerybeat') | |||||
def run(self): | |||||
print(str(self.colored.cyan( | |||||
'celery beat v{0} is starting.'.format(VERSION_BANNER)))) | |||||
self.init_loader() | |||||
self.set_process_title() | |||||
self.start_scheduler() | |||||
def setup_logging(self, colorize=None): | |||||
if colorize is None and self.no_color is not None: | |||||
colorize = not self.no_color | |||||
self.app.log.setup(self.loglevel, self.logfile, | |||||
self.redirect_stdouts, self.redirect_stdouts_level, | |||||
colorize=colorize) | |||||
def start_scheduler(self): | |||||
c = self.colored | |||||
if self.pidfile: | |||||
platforms.create_pidlock(self.pidfile) | |||||
beat = self.Service(app=self.app, | |||||
max_interval=self.max_interval, | |||||
scheduler_cls=self.scheduler_cls, | |||||
schedule_filename=self.schedule) | |||||
print(str(c.blue('__ ', c.magenta('-'), | |||||
c.blue(' ... __ '), c.magenta('-'), | |||||
c.blue(' _\n'), | |||||
c.reset(self.startup_info(beat))))) | |||||
self.setup_logging() | |||||
if self.socket_timeout: | |||||
logger.debug('Setting default socket timeout to %r', | |||||
self.socket_timeout) | |||||
socket.setdefaulttimeout(self.socket_timeout) | |||||
try: | |||||
self.install_sync_handler(beat) | |||||
beat.start() | |||||
except Exception as exc: | |||||
logger.critical('beat raised exception %s: %r', | |||||
exc.__class__, exc, | |||||
exc_info=True) | |||||
def init_loader(self): | |||||
# Run the worker init handler. | |||||
# (Usually imports task modules and such.) | |||||
self.app.loader.init_worker() | |||||
self.app.finalize() | |||||
def startup_info(self, beat): | |||||
scheduler = beat.get_scheduler(lazy=True) | |||||
return STARTUP_INFO_FMT.format( | |||||
conninfo=self.app.connection().as_uri(), | |||||
logfile=self.logfile or '[stderr]', | |||||
loglevel=LOG_LEVELS[self.loglevel], | |||||
loader=qualname(self.app.loader), | |||||
scheduler=qualname(scheduler), | |||||
scheduler_info=scheduler.info, | |||||
hmax_interval=humanize_seconds(beat.max_interval), | |||||
max_interval=beat.max_interval, | |||||
) | |||||
def set_process_title(self): | |||||
arg_start = 'manage' in sys.argv[0] and 2 or 1 | |||||
platforms.set_process_title( | |||||
'celery beat', info=' '.join(sys.argv[arg_start:]), | |||||
) | |||||
def install_sync_handler(self, beat): | |||||
"""Install a `SIGTERM` + `SIGINT` handler that saves | |||||
the beat schedule.""" | |||||
def _sync(signum, frame): | |||||
beat.sync() | |||||
raise SystemExit() | |||||
platforms.signals.update(SIGTERM=_sync, SIGINT=_sync) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.apps.worker | |||||
~~~~~~~~~~~~~~~~~~ | |||||
This module is the 'program-version' of :mod:`celery.worker`. | |||||
It does everything necessary to run that module | |||||
as an actual application, like installing signal handlers, | |||||
platform tweaks, and so on. | |||||
""" | |||||
from __future__ import absolute_import, print_function, unicode_literals | |||||
import logging | |||||
import os | |||||
import platform as _platform | |||||
import sys | |||||
import warnings | |||||
from functools import partial | |||||
from billiard import current_process | |||||
from kombu.utils.encoding import safe_str | |||||
from celery import VERSION_BANNER, platforms, signals | |||||
from celery.app import trace | |||||
from celery.exceptions import ( | |||||
CDeprecationWarning, WorkerShutdown, WorkerTerminate, | |||||
) | |||||
from celery.five import string, string_t | |||||
from celery.loaders.app import AppLoader | |||||
from celery.platforms import check_privileges | |||||
from celery.utils import cry, isatty | |||||
from celery.utils.imports import qualname | |||||
from celery.utils.log import get_logger, in_sighandler, set_in_sighandler | |||||
from celery.utils.text import pluralize | |||||
from celery.worker import WorkController | |||||
__all__ = ['Worker'] | |||||
logger = get_logger(__name__) | |||||
is_jython = sys.platform.startswith('java') | |||||
is_pypy = hasattr(sys, 'pypy_version_info') | |||||
W_PICKLE_DEPRECATED = """ | |||||
Starting from version 3.2 Celery will refuse to accept pickle by default. | |||||
The pickle serializer is a security concern as it may give attackers | |||||
the ability to execute any command. It's important to secure | |||||
your broker from unauthorized access when using pickle, so we think | |||||
that enabling pickle should require a deliberate action and not be | |||||
the default choice. | |||||
If you depend on pickle then you should set a setting to disable this | |||||
warning and to be sure that everything will continue working | |||||
when you upgrade to Celery 3.2:: | |||||
CELERY_ACCEPT_CONTENT = ['pickle', 'json', 'msgpack', 'yaml'] | |||||
You must only enable the serializers that you will actually use. | |||||
""" | |||||
def active_thread_count(): | |||||
from threading import enumerate | |||||
return sum(1 for t in enumerate() | |||||
if not t.name.startswith('Dummy-')) | |||||
def safe_say(msg): | |||||
print('\n{0}'.format(msg), file=sys.__stderr__) | |||||
ARTLINES = [ | |||||
' --------------', | |||||
'---- **** -----', | |||||
'--- * *** * --', | |||||
'-- * - **** ---', | |||||
'- ** ----------', | |||||
'- ** ----------', | |||||
'- ** ----------', | |||||
'- ** ----------', | |||||
'- *** --- * ---', | |||||
'-- ******* ----', | |||||
'--- ***** -----', | |||||
' --------------', | |||||
] | |||||
BANNER = """\ | |||||
{hostname} v{version} | |||||
{platform} | |||||
[config] | |||||
.> app: {app} | |||||
.> transport: {conninfo} | |||||
.> results: {results} | |||||
.> concurrency: {concurrency} | |||||
[queues] | |||||
{queues} | |||||
""" | |||||
EXTRA_INFO_FMT = """ | |||||
[tasks] | |||||
{tasks} | |||||
""" | |||||
class Worker(WorkController): | |||||
def on_before_init(self, **kwargs): | |||||
trace.setup_worker_optimizations(self.app) | |||||
# this signal can be used to set up configuration for | |||||
# workers by name. | |||||
signals.celeryd_init.send( | |||||
sender=self.hostname, instance=self, | |||||
conf=self.app.conf, options=kwargs, | |||||
) | |||||
check_privileges(self.app.conf.CELERY_ACCEPT_CONTENT) | |||||
def on_after_init(self, purge=False, no_color=None, | |||||
redirect_stdouts=None, redirect_stdouts_level=None, | |||||
**kwargs): | |||||
self.redirect_stdouts = self._getopt( | |||||
'redirect_stdouts', redirect_stdouts, | |||||
) | |||||
self.redirect_stdouts_level = self._getopt( | |||||
'redirect_stdouts_level', redirect_stdouts_level, | |||||
) | |||||
super(Worker, self).setup_defaults(**kwargs) | |||||
self.purge = purge | |||||
self.no_color = no_color | |||||
self._isatty = isatty(sys.stdout) | |||||
self.colored = self.app.log.colored( | |||||
self.logfile, | |||||
enabled=not no_color if no_color is not None else no_color | |||||
) | |||||
def on_init_blueprint(self): | |||||
self._custom_logging = self.setup_logging() | |||||
# apply task execution optimizations | |||||
# -- This will finalize the app! | |||||
trace.setup_worker_optimizations(self.app) | |||||
def on_start(self): | |||||
if not self._custom_logging and self.redirect_stdouts: | |||||
self.app.log.redirect_stdouts(self.redirect_stdouts_level) | |||||
WorkController.on_start(self) | |||||
# this signal can be used to e.g. change queues after | |||||
# the -Q option has been applied. | |||||
signals.celeryd_after_setup.send( | |||||
sender=self.hostname, instance=self, conf=self.app.conf, | |||||
) | |||||
if not self.app.conf.value_set_for('CELERY_ACCEPT_CONTENT'): | |||||
warnings.warn(CDeprecationWarning(W_PICKLE_DEPRECATED)) | |||||
if self.purge: | |||||
self.purge_messages() | |||||
# Dump configuration to screen so we have some basic information | |||||
# for when users sends bug reports. | |||||
print(safe_str(''.join([ | |||||
string(self.colored.cyan(' \n', self.startup_info())), | |||||
string(self.colored.reset(self.extra_info() or '')), | |||||
])), file=sys.__stdout__) | |||||
self.set_process_status('-active-') | |||||
self.install_platform_tweaks(self) | |||||
def on_consumer_ready(self, consumer): | |||||
signals.worker_ready.send(sender=consumer) | |||||
print('{0} ready.'.format(safe_str(self.hostname), )) | |||||
def setup_logging(self, colorize=None): | |||||
if colorize is None and self.no_color is not None: | |||||
colorize = not self.no_color | |||||
return self.app.log.setup( | |||||
self.loglevel, self.logfile, | |||||
redirect_stdouts=False, colorize=colorize, hostname=self.hostname, | |||||
) | |||||
def purge_messages(self): | |||||
count = self.app.control.purge() | |||||
if count: | |||||
print('purge: Erased {0} {1} from the queue.\n'.format( | |||||
count, pluralize(count, 'message'))) | |||||
def tasklist(self, include_builtins=True, sep='\n', int_='celery.'): | |||||
return sep.join( | |||||
' . {0}'.format(task) for task in sorted(self.app.tasks) | |||||
if (not task.startswith(int_) if not include_builtins else task) | |||||
) | |||||
def extra_info(self): | |||||
if self.loglevel <= logging.INFO: | |||||
include_builtins = self.loglevel <= logging.DEBUG | |||||
tasklist = self.tasklist(include_builtins=include_builtins) | |||||
return EXTRA_INFO_FMT.format(tasks=tasklist) | |||||
def startup_info(self): | |||||
app = self.app | |||||
concurrency = string(self.concurrency) | |||||
appr = '{0}:0x{1:x}'.format(app.main or '__main__', id(app)) | |||||
if not isinstance(app.loader, AppLoader): | |||||
loader = qualname(app.loader) | |||||
if loader.startswith('celery.loaders'): | |||||
loader = loader[14:] | |||||
appr += ' ({0})'.format(loader) | |||||
if self.autoscale: | |||||
max, min = self.autoscale | |||||
concurrency = '{{min={0}, max={1}}}'.format(min, max) | |||||
pool = self.pool_cls | |||||
if not isinstance(pool, string_t): | |||||
pool = pool.__module__ | |||||
concurrency += ' ({0})'.format(pool.split('.')[-1]) | |||||
events = 'ON' | |||||
if not self.send_events: | |||||
events = 'OFF (enable -E to monitor this worker)' | |||||
banner = BANNER.format( | |||||
app=appr, | |||||
hostname=safe_str(self.hostname), | |||||
version=VERSION_BANNER, | |||||
conninfo=self.app.connection().as_uri(), | |||||
results=self.app.backend.as_uri(), | |||||
concurrency=concurrency, | |||||
platform=safe_str(_platform.platform()), | |||||
events=events, | |||||
queues=app.amqp.queues.format(indent=0, indent_first=False), | |||||
).splitlines() | |||||
# integrate the ASCII art. | |||||
for i, x in enumerate(banner): | |||||
try: | |||||
banner[i] = ' '.join([ARTLINES[i], banner[i]]) | |||||
except IndexError: | |||||
banner[i] = ' ' * 16 + banner[i] | |||||
return '\n'.join(banner) + '\n' | |||||
def install_platform_tweaks(self, worker): | |||||
"""Install platform specific tweaks and workarounds.""" | |||||
if self.app.IS_OSX: | |||||
self.osx_proxy_detection_workaround() | |||||
# Install signal handler so SIGHUP restarts the worker. | |||||
if not self._isatty: | |||||
# only install HUP handler if detached from terminal, | |||||
# so closing the terminal window doesn't restart the worker | |||||
# into the background. | |||||
if self.app.IS_OSX: | |||||
# OS X can't exec from a process using threads. | |||||
# See http://github.com/celery/celery/issues#issue/152 | |||||
install_HUP_not_supported_handler(worker) | |||||
else: | |||||
install_worker_restart_handler(worker) | |||||
install_worker_term_handler(worker) | |||||
install_worker_term_hard_handler(worker) | |||||
install_worker_int_handler(worker) | |||||
install_cry_handler() | |||||
install_rdb_handler() | |||||
def osx_proxy_detection_workaround(self): | |||||
"""See http://github.com/celery/celery/issues#issue/161""" | |||||
os.environ.setdefault('celery_dummy_proxy', 'set_by_celeryd') | |||||
def set_process_status(self, info): | |||||
return platforms.set_mp_process_title( | |||||
'celeryd', | |||||
info='{0} ({1})'.format(info, platforms.strargv(sys.argv)), | |||||
hostname=self.hostname, | |||||
) | |||||
def _shutdown_handler(worker, sig='TERM', how='Warm', | |||||
exc=WorkerShutdown, callback=None): | |||||
def _handle_request(*args): | |||||
with in_sighandler(): | |||||
from celery.worker import state | |||||
if current_process()._name == 'MainProcess': | |||||
if callback: | |||||
callback(worker) | |||||
safe_say('worker: {0} shutdown (MainProcess)'.format(how)) | |||||
if active_thread_count() > 1: | |||||
setattr(state, {'Warm': 'should_stop', | |||||
'Cold': 'should_terminate'}[how], True) | |||||
else: | |||||
raise exc() | |||||
_handle_request.__name__ = str('worker_{0}'.format(how)) | |||||
platforms.signals[sig] = _handle_request | |||||
install_worker_term_handler = partial( | |||||
_shutdown_handler, sig='SIGTERM', how='Warm', exc=WorkerShutdown, | |||||
) | |||||
if not is_jython: # pragma: no cover | |||||
install_worker_term_hard_handler = partial( | |||||
_shutdown_handler, sig='SIGQUIT', how='Cold', exc=WorkerTerminate, | |||||
) | |||||
else: # pragma: no cover | |||||
install_worker_term_handler = \ | |||||
install_worker_term_hard_handler = lambda *a, **kw: None | |||||
def on_SIGINT(worker): | |||||
safe_say('worker: Hitting Ctrl+C again will terminate all running tasks!') | |||||
install_worker_term_hard_handler(worker, sig='SIGINT') | |||||
if not is_jython: # pragma: no cover | |||||
install_worker_int_handler = partial( | |||||
_shutdown_handler, sig='SIGINT', callback=on_SIGINT | |||||
) | |||||
else: # pragma: no cover | |||||
def install_worker_int_handler(*a, **kw): | |||||
pass | |||||
def _reload_current_worker(): | |||||
platforms.close_open_fds([ | |||||
sys.__stdin__, sys.__stdout__, sys.__stderr__, | |||||
]) | |||||
os.execv(sys.executable, [sys.executable] + sys.argv) | |||||
def install_worker_restart_handler(worker, sig='SIGHUP'): | |||||
def restart_worker_sig_handler(*args): | |||||
"""Signal handler restarting the current python program.""" | |||||
set_in_sighandler(True) | |||||
safe_say('Restarting celery worker ({0})'.format(' '.join(sys.argv))) | |||||
import atexit | |||||
atexit.register(_reload_current_worker) | |||||
from celery.worker import state | |||||
state.should_stop = True | |||||
platforms.signals[sig] = restart_worker_sig_handler | |||||
def install_cry_handler(sig='SIGUSR1'): | |||||
# Jython/PyPy does not have sys._current_frames | |||||
if is_jython or is_pypy: # pragma: no cover | |||||
return | |||||
def cry_handler(*args): | |||||
"""Signal handler logging the stacktrace of all active threads.""" | |||||
with in_sighandler(): | |||||
safe_say(cry()) | |||||
platforms.signals[sig] = cry_handler | |||||
def install_rdb_handler(envvar='CELERY_RDBSIG', | |||||
sig='SIGUSR2'): # pragma: no cover | |||||
def rdb_handler(*args): | |||||
"""Signal handler setting a rdb breakpoint at the current frame.""" | |||||
with in_sighandler(): | |||||
from celery.contrib.rdb import set_trace, _frame | |||||
# gevent does not pass standard signal handler args | |||||
frame = args[1] if args else _frame().f_back | |||||
set_trace(frame) | |||||
if os.environ.get(envvar): | |||||
platforms.signals[sig] = rdb_handler | |||||
def install_HUP_not_supported_handler(worker, sig='SIGHUP'): | |||||
def warn_on_HUP_handler(signum, frame): | |||||
with in_sighandler(): | |||||
safe_say('{sig} not supported: Restarting with {sig} is ' | |||||
'unstable on this platform!'.format(sig=sig)) | |||||
platforms.signals[sig] = warn_on_HUP_handler |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends | |||||
~~~~~~~~~~~~~~~ | |||||
Backend abstract factory (...did I just say that?) and alias definitions. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import sys | |||||
import types | |||||
from celery.exceptions import ImproperlyConfigured | |||||
from celery.local import Proxy | |||||
from celery._state import current_app | |||||
from celery.five import reraise | |||||
from celery.utils.imports import symbol_by_name | |||||
__all__ = ['get_backend_cls', 'get_backend_by_url'] | |||||
UNKNOWN_BACKEND = """\ | |||||
Unknown result backend: {0!r}. Did you spell that correctly? ({1!r})\ | |||||
""" | |||||
BACKEND_ALIASES = { | |||||
'amqp': 'celery.backends.amqp:AMQPBackend', | |||||
'rpc': 'celery.backends.rpc.RPCBackend', | |||||
'cache': 'celery.backends.cache:CacheBackend', | |||||
'redis': 'celery.backends.redis:RedisBackend', | |||||
'mongodb': 'celery.backends.mongodb:MongoBackend', | |||||
'db': 'celery.backends.database:DatabaseBackend', | |||||
'database': 'celery.backends.database:DatabaseBackend', | |||||
'cassandra': 'celery.backends.cassandra:CassandraBackend', | |||||
'couchbase': 'celery.backends.couchbase:CouchBaseBackend', | |||||
'disabled': 'celery.backends.base:DisabledBackend', | |||||
} | |||||
#: deprecated alias to ``current_app.backend``. | |||||
default_backend = Proxy(lambda: current_app.backend) | |||||
def get_backend_cls(backend=None, loader=None): | |||||
"""Get backend class by name/alias""" | |||||
backend = backend or 'disabled' | |||||
loader = loader or current_app.loader | |||||
aliases = dict(BACKEND_ALIASES, **loader.override_backends) | |||||
try: | |||||
cls = symbol_by_name(backend, aliases) | |||||
except ValueError as exc: | |||||
reraise(ImproperlyConfigured, ImproperlyConfigured( | |||||
UNKNOWN_BACKEND.format(backend, exc)), sys.exc_info()[2]) | |||||
if isinstance(cls, types.ModuleType): | |||||
raise ImproperlyConfigured(UNKNOWN_BACKEND.format( | |||||
backend, 'is a Python module, not a backend class.')) | |||||
return cls | |||||
def get_backend_by_url(backend=None, loader=None): | |||||
url = None | |||||
if backend and '://' in backend: | |||||
url = backend | |||||
scheme, _, _ = url.partition('://') | |||||
if '+' in scheme: | |||||
backend, url = url.split('+', 1) | |||||
else: | |||||
backend = scheme | |||||
return get_backend_cls(backend, loader), url |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.amqp | |||||
~~~~~~~~~~~~~~~~~~~~ | |||||
The AMQP result backend. | |||||
This backend publishes results as messages. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import socket | |||||
from collections import deque | |||||
from operator import itemgetter | |||||
from kombu import Exchange, Queue, Producer, Consumer | |||||
from celery import states | |||||
from celery.exceptions import TimeoutError | |||||
from celery.five import range, monotonic | |||||
from celery.utils.functional import dictfilter | |||||
from celery.utils.log import get_logger | |||||
from celery.utils.timeutils import maybe_s_to_ms | |||||
from .base import BaseBackend | |||||
__all__ = ['BacklogLimitExceeded', 'AMQPBackend'] | |||||
logger = get_logger(__name__) | |||||
class BacklogLimitExceeded(Exception): | |||||
"""Too much state history to fast-forward.""" | |||||
def repair_uuid(s): | |||||
# Historically the dashes in UUIDS are removed from AMQ entity names, | |||||
# but there is no known reason to. Hopefully we'll be able to fix | |||||
# this in v4.0. | |||||
return '%s-%s-%s-%s-%s' % (s[:8], s[8:12], s[12:16], s[16:20], s[20:]) | |||||
class NoCacheQueue(Queue): | |||||
can_cache_declaration = False | |||||
class AMQPBackend(BaseBackend): | |||||
"""Publishes results by sending messages.""" | |||||
Exchange = Exchange | |||||
Queue = NoCacheQueue | |||||
Consumer = Consumer | |||||
Producer = Producer | |||||
BacklogLimitExceeded = BacklogLimitExceeded | |||||
persistent = True | |||||
supports_autoexpire = True | |||||
supports_native_join = True | |||||
retry_policy = { | |||||
'max_retries': 20, | |||||
'interval_start': 0, | |||||
'interval_step': 1, | |||||
'interval_max': 1, | |||||
} | |||||
def __init__(self, app, connection=None, exchange=None, exchange_type=None, | |||||
persistent=None, serializer=None, auto_delete=True, **kwargs): | |||||
super(AMQPBackend, self).__init__(app, **kwargs) | |||||
conf = self.app.conf | |||||
self._connection = connection | |||||
self.persistent = self.prepare_persistent(persistent) | |||||
self.delivery_mode = 2 if self.persistent else 1 | |||||
exchange = exchange or conf.CELERY_RESULT_EXCHANGE | |||||
exchange_type = exchange_type or conf.CELERY_RESULT_EXCHANGE_TYPE | |||||
self.exchange = self._create_exchange( | |||||
exchange, exchange_type, self.delivery_mode, | |||||
) | |||||
self.serializer = serializer or conf.CELERY_RESULT_SERIALIZER | |||||
self.auto_delete = auto_delete | |||||
self.expires = None | |||||
if 'expires' not in kwargs or kwargs['expires'] is not None: | |||||
self.expires = self.prepare_expires(kwargs.get('expires')) | |||||
self.queue_arguments = dictfilter({ | |||||
'x-expires': maybe_s_to_ms(self.expires), | |||||
}) | |||||
def _create_exchange(self, name, type='direct', delivery_mode=2): | |||||
return self.Exchange(name=name, | |||||
type=type, | |||||
delivery_mode=delivery_mode, | |||||
durable=self.persistent, | |||||
auto_delete=False) | |||||
def _create_binding(self, task_id): | |||||
name = self.rkey(task_id) | |||||
return self.Queue(name=name, | |||||
exchange=self.exchange, | |||||
routing_key=name, | |||||
durable=self.persistent, | |||||
auto_delete=self.auto_delete, | |||||
queue_arguments=self.queue_arguments) | |||||
def revive(self, channel): | |||||
pass | |||||
def rkey(self, task_id): | |||||
return task_id.replace('-', '') | |||||
def destination_for(self, task_id, request): | |||||
if request: | |||||
return self.rkey(task_id), request.correlation_id or task_id | |||||
return self.rkey(task_id), task_id | |||||
def store_result(self, task_id, result, status, | |||||
traceback=None, request=None, **kwargs): | |||||
"""Send task return value and status.""" | |||||
routing_key, correlation_id = self.destination_for(task_id, request) | |||||
if not routing_key: | |||||
return | |||||
with self.app.amqp.producer_pool.acquire(block=True) as producer: | |||||
producer.publish( | |||||
{'task_id': task_id, 'status': status, | |||||
'result': self.encode_result(result, status), | |||||
'traceback': traceback, | |||||
'children': self.current_task_children(request)}, | |||||
exchange=self.exchange, | |||||
routing_key=routing_key, | |||||
correlation_id=correlation_id, | |||||
serializer=self.serializer, | |||||
retry=True, retry_policy=self.retry_policy, | |||||
declare=self.on_reply_declare(task_id), | |||||
delivery_mode=self.delivery_mode, | |||||
) | |||||
return result | |||||
def on_reply_declare(self, task_id): | |||||
return [self._create_binding(task_id)] | |||||
def wait_for(self, task_id, timeout=None, cache=True, | |||||
no_ack=True, on_interval=None, | |||||
READY_STATES=states.READY_STATES, | |||||
PROPAGATE_STATES=states.PROPAGATE_STATES, | |||||
**kwargs): | |||||
cached_meta = self._cache.get(task_id) | |||||
if cache and cached_meta and \ | |||||
cached_meta['status'] in READY_STATES: | |||||
return cached_meta | |||||
else: | |||||
try: | |||||
return self.consume(task_id, timeout=timeout, no_ack=no_ack, | |||||
on_interval=on_interval) | |||||
except socket.timeout: | |||||
raise TimeoutError('The operation timed out.') | |||||
def get_task_meta(self, task_id, backlog_limit=1000): | |||||
# Polling and using basic_get | |||||
with self.app.pool.acquire_channel(block=True) as (_, channel): | |||||
binding = self._create_binding(task_id)(channel) | |||||
binding.declare() | |||||
prev = latest = acc = None | |||||
for i in range(backlog_limit): # spool ffwd | |||||
acc = binding.get( | |||||
accept=self.accept, no_ack=False, | |||||
) | |||||
if not acc: # no more messages | |||||
break | |||||
if acc.payload['task_id'] == task_id: | |||||
prev, latest = latest, acc | |||||
if prev: | |||||
# backends are not expected to keep history, | |||||
# so we delete everything except the most recent state. | |||||
prev.ack() | |||||
prev = None | |||||
else: | |||||
raise self.BacklogLimitExceeded(task_id) | |||||
if latest: | |||||
payload = self._cache[task_id] = \ | |||||
self.meta_from_decoded(latest.payload) | |||||
latest.requeue() | |||||
return payload | |||||
else: | |||||
# no new state, use previous | |||||
try: | |||||
return self._cache[task_id] | |||||
except KeyError: | |||||
# result probably pending. | |||||
return {'status': states.PENDING, 'result': None} | |||||
poll = get_task_meta # XXX compat | |||||
def drain_events(self, connection, consumer, | |||||
timeout=None, on_interval=None, now=monotonic, wait=None): | |||||
wait = wait or connection.drain_events | |||||
results = {} | |||||
def callback(meta, message): | |||||
if meta['status'] in states.READY_STATES: | |||||
results[meta['task_id']] = self.meta_from_decoded(meta) | |||||
consumer.callbacks[:] = [callback] | |||||
time_start = now() | |||||
while 1: | |||||
# Total time spent may exceed a single call to wait() | |||||
if timeout and now() - time_start >= timeout: | |||||
raise socket.timeout() | |||||
try: | |||||
wait(timeout=1) | |||||
except socket.timeout: | |||||
pass | |||||
if on_interval: | |||||
on_interval() | |||||
if results: # got event on the wanted channel. | |||||
break | |||||
self._cache.update(results) | |||||
return results | |||||
def consume(self, task_id, timeout=None, no_ack=True, on_interval=None): | |||||
wait = self.drain_events | |||||
with self.app.pool.acquire_channel(block=True) as (conn, channel): | |||||
binding = self._create_binding(task_id) | |||||
with self.Consumer(channel, binding, | |||||
no_ack=no_ack, accept=self.accept) as consumer: | |||||
while 1: | |||||
try: | |||||
return wait( | |||||
conn, consumer, timeout, on_interval)[task_id] | |||||
except KeyError: | |||||
continue | |||||
def _many_bindings(self, ids): | |||||
return [self._create_binding(task_id) for task_id in ids] | |||||
def get_many(self, task_ids, timeout=None, no_ack=True, | |||||
now=monotonic, getfields=itemgetter('status', 'task_id'), | |||||
READY_STATES=states.READY_STATES, | |||||
PROPAGATE_STATES=states.PROPAGATE_STATES, **kwargs): | |||||
with self.app.pool.acquire_channel(block=True) as (conn, channel): | |||||
ids = set(task_ids) | |||||
cached_ids = set() | |||||
mark_cached = cached_ids.add | |||||
for task_id in ids: | |||||
try: | |||||
cached = self._cache[task_id] | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
if cached['status'] in READY_STATES: | |||||
yield task_id, cached | |||||
mark_cached(task_id) | |||||
ids.difference_update(cached_ids) | |||||
results = deque() | |||||
push_result = results.append | |||||
push_cache = self._cache.__setitem__ | |||||
decode_result = self.meta_from_decoded | |||||
def on_message(message): | |||||
body = decode_result(message.decode()) | |||||
state, uid = getfields(body) | |||||
if state in READY_STATES: | |||||
push_result(body) \ | |||||
if uid in task_ids else push_cache(uid, body) | |||||
bindings = self._many_bindings(task_ids) | |||||
with self.Consumer(channel, bindings, on_message=on_message, | |||||
accept=self.accept, no_ack=no_ack): | |||||
wait = conn.drain_events | |||||
popleft = results.popleft | |||||
while ids: | |||||
wait(timeout=timeout) | |||||
while results: | |||||
state = popleft() | |||||
task_id = state['task_id'] | |||||
ids.discard(task_id) | |||||
push_cache(task_id, state) | |||||
yield task_id, state | |||||
def reload_task_result(self, task_id): | |||||
raise NotImplementedError( | |||||
'reload_task_result is not supported by this backend.') | |||||
def reload_group_result(self, task_id): | |||||
"""Reload group result, even if it has been previously fetched.""" | |||||
raise NotImplementedError( | |||||
'reload_group_result is not supported by this backend.') | |||||
def save_group(self, group_id, result): | |||||
raise NotImplementedError( | |||||
'save_group is not supported by this backend.') | |||||
def restore_group(self, group_id, cache=True): | |||||
raise NotImplementedError( | |||||
'restore_group is not supported by this backend.') | |||||
def delete_group(self, group_id): | |||||
raise NotImplementedError( | |||||
'delete_group is not supported by this backend.') | |||||
def as_uri(self, include_password=True): | |||||
return 'amqp://' | |||||
def __reduce__(self, args=(), kwargs={}): | |||||
kwargs.update( | |||||
connection=self._connection, | |||||
exchange=self.exchange.name, | |||||
exchange_type=self.exchange.type, | |||||
persistent=self.persistent, | |||||
serializer=self.serializer, | |||||
auto_delete=self.auto_delete, | |||||
expires=self.expires, | |||||
) | |||||
return super(AMQPBackend, self).__reduce__(args, kwargs) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.base | |||||
~~~~~~~~~~~~~~~~~~~~ | |||||
Result backend base classes. | |||||
- :class:`BaseBackend` defines the interface. | |||||
- :class:`KeyValueStoreBackend` is a common base class | |||||
using K/V semantics like _get and _put. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import time | |||||
import sys | |||||
from datetime import timedelta | |||||
from billiard.einfo import ExceptionInfo | |||||
from kombu.serialization import ( | |||||
dumps, loads, prepare_accept_content, | |||||
registry as serializer_registry, | |||||
) | |||||
from kombu.utils.encoding import bytes_to_str, ensure_bytes, from_utf8 | |||||
from kombu.utils.url import maybe_sanitize_url | |||||
from celery import states | |||||
from celery import current_app, maybe_signature | |||||
from celery.app import current_task | |||||
from celery.exceptions import ChordError, TimeoutError, TaskRevokedError | |||||
from celery.five import items | |||||
from celery.result import ( | |||||
GroupResult, ResultBase, allow_join_result, result_from_tuple, | |||||
) | |||||
from celery.utils import timeutils | |||||
from celery.utils.functional import LRUCache | |||||
from celery.utils.log import get_logger | |||||
from celery.utils.serialization import ( | |||||
get_pickled_exception, | |||||
get_pickleable_exception, | |||||
create_exception_cls, | |||||
) | |||||
__all__ = ['BaseBackend', 'KeyValueStoreBackend', 'DisabledBackend'] | |||||
EXCEPTION_ABLE_CODECS = frozenset(['pickle']) | |||||
PY3 = sys.version_info >= (3, 0) | |||||
logger = get_logger(__name__) | |||||
def unpickle_backend(cls, args, kwargs): | |||||
"""Return an unpickled backend.""" | |||||
return cls(*args, app=current_app._get_current_object(), **kwargs) | |||||
class _nulldict(dict): | |||||
def ignore(self, *a, **kw): | |||||
pass | |||||
__setitem__ = update = setdefault = ignore | |||||
class BaseBackend(object): | |||||
READY_STATES = states.READY_STATES | |||||
UNREADY_STATES = states.UNREADY_STATES | |||||
EXCEPTION_STATES = states.EXCEPTION_STATES | |||||
TimeoutError = TimeoutError | |||||
#: Time to sleep between polling each individual item | |||||
#: in `ResultSet.iterate`. as opposed to the `interval` | |||||
#: argument which is for each pass. | |||||
subpolling_interval = None | |||||
#: If true the backend must implement :meth:`get_many`. | |||||
supports_native_join = False | |||||
#: If true the backend must automatically expire results. | |||||
#: The daily backend_cleanup periodic task will not be triggered | |||||
#: in this case. | |||||
supports_autoexpire = False | |||||
#: Set to true if the backend is peristent by default. | |||||
persistent = True | |||||
retry_policy = { | |||||
'max_retries': 20, | |||||
'interval_start': 0, | |||||
'interval_step': 1, | |||||
'interval_max': 1, | |||||
} | |||||
def __init__(self, app, | |||||
serializer=None, max_cached_results=None, accept=None, | |||||
url=None, **kwargs): | |||||
self.app = app | |||||
conf = self.app.conf | |||||
self.serializer = serializer or conf.CELERY_RESULT_SERIALIZER | |||||
(self.content_type, | |||||
self.content_encoding, | |||||
self.encoder) = serializer_registry._encoders[self.serializer] | |||||
cmax = max_cached_results or conf.CELERY_MAX_CACHED_RESULTS | |||||
self._cache = _nulldict() if cmax == -1 else LRUCache(limit=cmax) | |||||
self.accept = prepare_accept_content( | |||||
conf.CELERY_ACCEPT_CONTENT if accept is None else accept, | |||||
) | |||||
self.url = url | |||||
def as_uri(self, include_password=False): | |||||
"""Return the backend as an URI, sanitizing the password or not""" | |||||
# when using maybe_sanitize_url(), "/" is added | |||||
# we're stripping it for consistency | |||||
if include_password: | |||||
return self.url | |||||
url = maybe_sanitize_url(self.url or '') | |||||
return url[:-1] if url.endswith(':///') else url | |||||
def mark_as_started(self, task_id, **meta): | |||||
"""Mark a task as started""" | |||||
return self.store_result(task_id, meta, status=states.STARTED) | |||||
def mark_as_done(self, task_id, result, request=None): | |||||
"""Mark task as successfully executed.""" | |||||
return self.store_result(task_id, result, | |||||
status=states.SUCCESS, request=request) | |||||
def mark_as_failure(self, task_id, exc, traceback=None, request=None): | |||||
"""Mark task as executed with failure. Stores the exception.""" | |||||
return self.store_result(task_id, exc, status=states.FAILURE, | |||||
traceback=traceback, request=request) | |||||
def chord_error_from_stack(self, callback, exc=None): | |||||
from celery import group | |||||
app = self.app | |||||
backend = app._tasks[callback.task].backend | |||||
try: | |||||
group( | |||||
[app.signature(errback) | |||||
for errback in callback.options.get('link_error') or []], | |||||
app=app, | |||||
).apply_async((callback.id, )) | |||||
except Exception as eb_exc: | |||||
return backend.fail_from_current_stack(callback.id, exc=eb_exc) | |||||
else: | |||||
return backend.fail_from_current_stack(callback.id, exc=exc) | |||||
def fail_from_current_stack(self, task_id, exc=None): | |||||
type_, real_exc, tb = sys.exc_info() | |||||
try: | |||||
exc = real_exc if exc is None else exc | |||||
ei = ExceptionInfo((type_, exc, tb)) | |||||
self.mark_as_failure(task_id, exc, ei.traceback) | |||||
return ei | |||||
finally: | |||||
del(tb) | |||||
def mark_as_retry(self, task_id, exc, traceback=None, request=None): | |||||
"""Mark task as being retries. Stores the current | |||||
exception (if any).""" | |||||
return self.store_result(task_id, exc, status=states.RETRY, | |||||
traceback=traceback, request=request) | |||||
def mark_as_revoked(self, task_id, reason='', request=None): | |||||
return self.store_result(task_id, TaskRevokedError(reason), | |||||
status=states.REVOKED, traceback=None, | |||||
request=request) | |||||
def prepare_exception(self, exc, serializer=None): | |||||
"""Prepare exception for serialization.""" | |||||
serializer = self.serializer if serializer is None else serializer | |||||
if serializer in EXCEPTION_ABLE_CODECS: | |||||
return get_pickleable_exception(exc) | |||||
return {'exc_type': type(exc).__name__, 'exc_message': str(exc)} | |||||
def exception_to_python(self, exc): | |||||
"""Convert serialized exception to Python exception.""" | |||||
if exc: | |||||
if not isinstance(exc, BaseException): | |||||
exc = create_exception_cls( | |||||
from_utf8(exc['exc_type']), __name__)(exc['exc_message']) | |||||
if self.serializer in EXCEPTION_ABLE_CODECS: | |||||
exc = get_pickled_exception(exc) | |||||
return exc | |||||
def prepare_value(self, result): | |||||
"""Prepare value for storage.""" | |||||
if self.serializer != 'pickle' and isinstance(result, ResultBase): | |||||
return result.as_tuple() | |||||
return result | |||||
def encode(self, data): | |||||
_, _, payload = dumps(data, serializer=self.serializer) | |||||
return payload | |||||
def meta_from_decoded(self, meta): | |||||
if meta['status'] in self.EXCEPTION_STATES: | |||||
meta['result'] = self.exception_to_python(meta['result']) | |||||
return meta | |||||
def decode_result(self, payload): | |||||
return self.meta_from_decoded(self.decode(payload)) | |||||
def decode(self, payload): | |||||
payload = PY3 and payload or str(payload) | |||||
return loads(payload, | |||||
content_type=self.content_type, | |||||
content_encoding=self.content_encoding, | |||||
accept=self.accept) | |||||
def wait_for(self, task_id, | |||||
timeout=None, interval=0.5, no_ack=True, on_interval=None): | |||||
"""Wait for task and return its result. | |||||
If the task raises an exception, this exception | |||||
will be re-raised by :func:`wait_for`. | |||||
If `timeout` is not :const:`None`, this raises the | |||||
:class:`celery.exceptions.TimeoutError` exception if the operation | |||||
takes longer than `timeout` seconds. | |||||
""" | |||||
time_elapsed = 0.0 | |||||
while 1: | |||||
meta = self.get_task_meta(task_id) | |||||
if meta['status'] in states.READY_STATES: | |||||
return meta | |||||
if on_interval: | |||||
on_interval() | |||||
# avoid hammering the CPU checking status. | |||||
time.sleep(interval) | |||||
time_elapsed += interval | |||||
if timeout and time_elapsed >= timeout: | |||||
raise TimeoutError('The operation timed out.') | |||||
def prepare_expires(self, value, type=None): | |||||
if value is None: | |||||
value = self.app.conf.CELERY_TASK_RESULT_EXPIRES | |||||
if isinstance(value, timedelta): | |||||
value = timeutils.timedelta_seconds(value) | |||||
if value is not None and type: | |||||
return type(value) | |||||
return value | |||||
def prepare_persistent(self, enabled=None): | |||||
if enabled is not None: | |||||
return enabled | |||||
p = self.app.conf.CELERY_RESULT_PERSISTENT | |||||
return self.persistent if p is None else p | |||||
def encode_result(self, result, status): | |||||
if isinstance(result, ExceptionInfo): | |||||
result = result.exception | |||||
if status in self.EXCEPTION_STATES and isinstance(result, Exception): | |||||
return self.prepare_exception(result) | |||||
else: | |||||
return self.prepare_value(result) | |||||
def is_cached(self, task_id): | |||||
return task_id in self._cache | |||||
def store_result(self, task_id, result, status, | |||||
traceback=None, request=None, **kwargs): | |||||
"""Update task state and result.""" | |||||
result = self.encode_result(result, status) | |||||
self._store_result(task_id, result, status, traceback, | |||||
request=request, **kwargs) | |||||
return result | |||||
def forget(self, task_id): | |||||
self._cache.pop(task_id, None) | |||||
self._forget(task_id) | |||||
def _forget(self, task_id): | |||||
raise NotImplementedError('backend does not implement forget.') | |||||
def get_status(self, task_id): | |||||
"""Get the status of a task.""" | |||||
return self.get_task_meta(task_id)['status'] | |||||
def get_traceback(self, task_id): | |||||
"""Get the traceback for a failed task.""" | |||||
return self.get_task_meta(task_id).get('traceback') | |||||
def get_result(self, task_id): | |||||
"""Get the result of a task.""" | |||||
return self.get_task_meta(task_id).get('result') | |||||
def get_children(self, task_id): | |||||
"""Get the list of subtasks sent by a task.""" | |||||
try: | |||||
return self.get_task_meta(task_id)['children'] | |||||
except KeyError: | |||||
pass | |||||
def get_task_meta(self, task_id, cache=True): | |||||
if cache: | |||||
try: | |||||
return self._cache[task_id] | |||||
except KeyError: | |||||
pass | |||||
meta = self._get_task_meta_for(task_id) | |||||
if cache and meta.get('status') == states.SUCCESS: | |||||
self._cache[task_id] = meta | |||||
return meta | |||||
def reload_task_result(self, task_id): | |||||
"""Reload task result, even if it has been previously fetched.""" | |||||
self._cache[task_id] = self.get_task_meta(task_id, cache=False) | |||||
def reload_group_result(self, group_id): | |||||
"""Reload group result, even if it has been previously fetched.""" | |||||
self._cache[group_id] = self.get_group_meta(group_id, cache=False) | |||||
def get_group_meta(self, group_id, cache=True): | |||||
if cache: | |||||
try: | |||||
return self._cache[group_id] | |||||
except KeyError: | |||||
pass | |||||
meta = self._restore_group(group_id) | |||||
if cache and meta is not None: | |||||
self._cache[group_id] = meta | |||||
return meta | |||||
def restore_group(self, group_id, cache=True): | |||||
"""Get the result for a group.""" | |||||
meta = self.get_group_meta(group_id, cache=cache) | |||||
if meta: | |||||
return meta['result'] | |||||
def save_group(self, group_id, result): | |||||
"""Store the result of an executed group.""" | |||||
return self._save_group(group_id, result) | |||||
def delete_group(self, group_id): | |||||
self._cache.pop(group_id, None) | |||||
return self._delete_group(group_id) | |||||
def cleanup(self): | |||||
"""Backend cleanup. Is run by | |||||
:class:`celery.task.DeleteExpiredTaskMetaTask`.""" | |||||
pass | |||||
def process_cleanup(self): | |||||
"""Cleanup actions to do at the end of a task worker process.""" | |||||
pass | |||||
def on_task_call(self, producer, task_id): | |||||
return {} | |||||
def on_chord_part_return(self, task, state, result, propagate=False): | |||||
pass | |||||
def fallback_chord_unlock(self, group_id, body, result=None, | |||||
countdown=1, **kwargs): | |||||
kwargs['result'] = [r.as_tuple() for r in result] | |||||
self.app.tasks['celery.chord_unlock'].apply_async( | |||||
(group_id, body, ), kwargs, countdown=countdown, | |||||
) | |||||
def apply_chord(self, header, partial_args, group_id, body, **options): | |||||
result = header(*partial_args, task_id=group_id) | |||||
self.fallback_chord_unlock(group_id, body, **options) | |||||
return result | |||||
def current_task_children(self, request=None): | |||||
request = request or getattr(current_task(), 'request', None) | |||||
if request: | |||||
return [r.as_tuple() for r in getattr(request, 'children', [])] | |||||
def __reduce__(self, args=(), kwargs={}): | |||||
return (unpickle_backend, (self.__class__, args, kwargs)) | |||||
BaseDictBackend = BaseBackend # XXX compat | |||||
class KeyValueStoreBackend(BaseBackend): | |||||
key_t = ensure_bytes | |||||
task_keyprefix = 'celery-task-meta-' | |||||
group_keyprefix = 'celery-taskset-meta-' | |||||
chord_keyprefix = 'chord-unlock-' | |||||
implements_incr = False | |||||
def __init__(self, *args, **kwargs): | |||||
if hasattr(self.key_t, '__func__'): | |||||
self.key_t = self.key_t.__func__ # remove binding | |||||
self._encode_prefixes() | |||||
super(KeyValueStoreBackend, self).__init__(*args, **kwargs) | |||||
if self.implements_incr: | |||||
self.apply_chord = self._apply_chord_incr | |||||
def _encode_prefixes(self): | |||||
self.task_keyprefix = self.key_t(self.task_keyprefix) | |||||
self.group_keyprefix = self.key_t(self.group_keyprefix) | |||||
self.chord_keyprefix = self.key_t(self.chord_keyprefix) | |||||
def get(self, key): | |||||
raise NotImplementedError('Must implement the get method.') | |||||
def mget(self, keys): | |||||
raise NotImplementedError('Does not support get_many') | |||||
def set(self, key, value): | |||||
raise NotImplementedError('Must implement the set method.') | |||||
def delete(self, key): | |||||
raise NotImplementedError('Must implement the delete method') | |||||
def incr(self, key): | |||||
raise NotImplementedError('Does not implement incr') | |||||
def expire(self, key, value): | |||||
pass | |||||
def get_key_for_task(self, task_id, key=''): | |||||
"""Get the cache key for a task by id.""" | |||||
key_t = self.key_t | |||||
return key_t('').join([ | |||||
self.task_keyprefix, key_t(task_id), key_t(key), | |||||
]) | |||||
def get_key_for_group(self, group_id, key=''): | |||||
"""Get the cache key for a group by id.""" | |||||
key_t = self.key_t | |||||
return key_t('').join([ | |||||
self.group_keyprefix, key_t(group_id), key_t(key), | |||||
]) | |||||
def get_key_for_chord(self, group_id, key=''): | |||||
"""Get the cache key for the chord waiting on group with given id.""" | |||||
key_t = self.key_t | |||||
return key_t('').join([ | |||||
self.chord_keyprefix, key_t(group_id), key_t(key), | |||||
]) | |||||
def _strip_prefix(self, key): | |||||
"""Takes bytes, emits string.""" | |||||
key = self.key_t(key) | |||||
for prefix in self.task_keyprefix, self.group_keyprefix: | |||||
if key.startswith(prefix): | |||||
return bytes_to_str(key[len(prefix):]) | |||||
return bytes_to_str(key) | |||||
def _filter_ready(self, values, READY_STATES=states.READY_STATES): | |||||
for k, v in values: | |||||
if v is not None: | |||||
v = self.decode_result(v) | |||||
if v['status'] in READY_STATES: | |||||
yield k, v | |||||
def _mget_to_results(self, values, keys): | |||||
if hasattr(values, 'items'): | |||||
# client returns dict so mapping preserved. | |||||
return dict((self._strip_prefix(k), v) | |||||
for k, v in self._filter_ready(items(values))) | |||||
else: | |||||
# client returns list so need to recreate mapping. | |||||
return dict((bytes_to_str(keys[i]), v) | |||||
for i, v in self._filter_ready(enumerate(values))) | |||||
def get_many(self, task_ids, timeout=None, interval=0.5, no_ack=True, | |||||
READY_STATES=states.READY_STATES): | |||||
interval = 0.5 if interval is None else interval | |||||
ids = task_ids if isinstance(task_ids, set) else set(task_ids) | |||||
cached_ids = set() | |||||
cache = self._cache | |||||
for task_id in ids: | |||||
try: | |||||
cached = cache[task_id] | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
if cached['status'] in READY_STATES: | |||||
yield bytes_to_str(task_id), cached | |||||
cached_ids.add(task_id) | |||||
ids.difference_update(cached_ids) | |||||
iterations = 0 | |||||
while ids: | |||||
keys = list(ids) | |||||
r = self._mget_to_results(self.mget([self.get_key_for_task(k) | |||||
for k in keys]), keys) | |||||
cache.update(r) | |||||
ids.difference_update(set(bytes_to_str(v) for v in r)) | |||||
for key, value in items(r): | |||||
yield bytes_to_str(key), value | |||||
if timeout and iterations * interval >= timeout: | |||||
raise TimeoutError('Operation timed out ({0})'.format(timeout)) | |||||
time.sleep(interval) # don't busy loop. | |||||
iterations += 1 | |||||
def _forget(self, task_id): | |||||
self.delete(self.get_key_for_task(task_id)) | |||||
def _store_result(self, task_id, result, status, | |||||
traceback=None, request=None, **kwargs): | |||||
meta = {'status': status, 'result': result, 'traceback': traceback, | |||||
'children': self.current_task_children(request)} | |||||
self.set(self.get_key_for_task(task_id), self.encode(meta)) | |||||
return result | |||||
def _save_group(self, group_id, result): | |||||
self.set(self.get_key_for_group(group_id), | |||||
self.encode({'result': result.as_tuple()})) | |||||
return result | |||||
def _delete_group(self, group_id): | |||||
self.delete(self.get_key_for_group(group_id)) | |||||
def _get_task_meta_for(self, task_id): | |||||
"""Get task metadata for a task by id.""" | |||||
meta = self.get(self.get_key_for_task(task_id)) | |||||
if not meta: | |||||
return {'status': states.PENDING, 'result': None} | |||||
return self.decode_result(meta) | |||||
def _restore_group(self, group_id): | |||||
"""Get task metadata for a task by id.""" | |||||
meta = self.get(self.get_key_for_group(group_id)) | |||||
# previously this was always pickled, but later this | |||||
# was extended to support other serializers, so the | |||||
# structure is kind of weird. | |||||
if meta: | |||||
meta = self.decode(meta) | |||||
result = meta['result'] | |||||
meta['result'] = result_from_tuple(result, self.app) | |||||
return meta | |||||
def _apply_chord_incr(self, header, partial_args, group_id, body, | |||||
result=None, **options): | |||||
self.save_group(group_id, self.app.GroupResult(group_id, result)) | |||||
return header(*partial_args, task_id=group_id) | |||||
def on_chord_part_return(self, task, state, result, propagate=None): | |||||
if not self.implements_incr: | |||||
return | |||||
app = self.app | |||||
if propagate is None: | |||||
propagate = app.conf.CELERY_CHORD_PROPAGATES | |||||
gid = task.request.group | |||||
if not gid: | |||||
return | |||||
key = self.get_key_for_chord(gid) | |||||
try: | |||||
deps = GroupResult.restore(gid, backend=task.backend) | |||||
except Exception as exc: | |||||
callback = maybe_signature(task.request.chord, app=app) | |||||
logger.error('Chord %r raised: %r', gid, exc, exc_info=1) | |||||
return self.chord_error_from_stack( | |||||
callback, | |||||
ChordError('Cannot restore group: {0!r}'.format(exc)), | |||||
) | |||||
if deps is None: | |||||
try: | |||||
raise ValueError(gid) | |||||
except ValueError as exc: | |||||
callback = maybe_signature(task.request.chord, app=app) | |||||
logger.error('Chord callback %r raised: %r', gid, exc, | |||||
exc_info=1) | |||||
return self.chord_error_from_stack( | |||||
callback, | |||||
ChordError('GroupResult {0} no longer exists'.format(gid)), | |||||
) | |||||
val = self.incr(key) | |||||
size = len(deps) | |||||
if val > size: | |||||
logger.warning('Chord counter incremented too many times for %r', | |||||
gid) | |||||
elif val == size: | |||||
callback = maybe_signature(task.request.chord, app=app) | |||||
j = deps.join_native if deps.supports_native_join else deps.join | |||||
try: | |||||
with allow_join_result(): | |||||
ret = j(timeout=3.0, propagate=propagate) | |||||
except Exception as exc: | |||||
try: | |||||
culprit = next(deps._failed_join_report()) | |||||
reason = 'Dependency {0.id} raised {1!r}'.format( | |||||
culprit, exc, | |||||
) | |||||
except StopIteration: | |||||
reason = repr(exc) | |||||
logger.error('Chord %r raised: %r', gid, reason, exc_info=1) | |||||
self.chord_error_from_stack(callback, ChordError(reason)) | |||||
else: | |||||
try: | |||||
callback.delay(ret) | |||||
except Exception as exc: | |||||
logger.error('Chord %r raised: %r', gid, exc, exc_info=1) | |||||
self.chord_error_from_stack( | |||||
callback, | |||||
ChordError('Callback error: {0!r}'.format(exc)), | |||||
) | |||||
finally: | |||||
deps.delete() | |||||
self.client.delete(key) | |||||
else: | |||||
self.expire(key, 86400) | |||||
class DisabledBackend(BaseBackend): | |||||
_cache = {} # need this attribute to reset cache in tests. | |||||
def store_result(self, *args, **kwargs): | |||||
pass | |||||
def _is_disabled(self, *args, **kwargs): | |||||
raise NotImplementedError( | |||||
'No result backend configured. ' | |||||
'Please see the documentation for more information.') | |||||
def as_uri(self, *args, **kwargs): | |||||
return 'disabled://' | |||||
get_state = get_status = get_result = get_traceback = _is_disabled | |||||
wait_for = get_many = _is_disabled |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.cache | |||||
~~~~~~~~~~~~~~~~~~~~~ | |||||
Memcache and in-memory cache result backend. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import sys | |||||
from kombu.utils import cached_property | |||||
from kombu.utils.encoding import bytes_to_str, ensure_bytes | |||||
from celery.exceptions import ImproperlyConfigured | |||||
from celery.utils.functional import LRUCache | |||||
from .base import KeyValueStoreBackend | |||||
__all__ = ['CacheBackend'] | |||||
_imp = [None] | |||||
PY3 = sys.version_info[0] == 3 | |||||
REQUIRES_BACKEND = """\ | |||||
The memcached backend requires either pylibmc or python-memcached.\ | |||||
""" | |||||
UNKNOWN_BACKEND = """\ | |||||
The cache backend {0!r} is unknown, | |||||
Please use one of the following backends instead: {1}\ | |||||
""" | |||||
def import_best_memcache(): | |||||
if _imp[0] is None: | |||||
is_pylibmc, memcache_key_t = False, ensure_bytes | |||||
try: | |||||
import pylibmc as memcache | |||||
is_pylibmc = True | |||||
except ImportError: | |||||
try: | |||||
import memcache # noqa | |||||
except ImportError: | |||||
raise ImproperlyConfigured(REQUIRES_BACKEND) | |||||
if PY3: | |||||
memcache_key_t = bytes_to_str | |||||
_imp[0] = (is_pylibmc, memcache, memcache_key_t) | |||||
return _imp[0] | |||||
def get_best_memcache(*args, **kwargs): | |||||
is_pylibmc, memcache, key_t = import_best_memcache() | |||||
Client = _Client = memcache.Client | |||||
if not is_pylibmc: | |||||
def Client(*args, **kwargs): # noqa | |||||
kwargs.pop('behaviors', None) | |||||
return _Client(*args, **kwargs) | |||||
return Client, key_t | |||||
class DummyClient(object): | |||||
def __init__(self, *args, **kwargs): | |||||
self.cache = LRUCache(limit=5000) | |||||
def get(self, key, *args, **kwargs): | |||||
return self.cache.get(key) | |||||
def get_multi(self, keys): | |||||
cache = self.cache | |||||
return dict((k, cache[k]) for k in keys if k in cache) | |||||
def set(self, key, value, *args, **kwargs): | |||||
self.cache[key] = value | |||||
def delete(self, key, *args, **kwargs): | |||||
self.cache.pop(key, None) | |||||
def incr(self, key, delta=1): | |||||
return self.cache.incr(key, delta) | |||||
backends = {'memcache': get_best_memcache, | |||||
'memcached': get_best_memcache, | |||||
'pylibmc': get_best_memcache, | |||||
'memory': lambda: (DummyClient, ensure_bytes)} | |||||
class CacheBackend(KeyValueStoreBackend): | |||||
servers = None | |||||
supports_autoexpire = True | |||||
supports_native_join = True | |||||
implements_incr = True | |||||
def __init__(self, app, expires=None, backend=None, | |||||
options={}, url=None, **kwargs): | |||||
super(CacheBackend, self).__init__(app, **kwargs) | |||||
self.url = url | |||||
self.options = dict(self.app.conf.CELERY_CACHE_BACKEND_OPTIONS, | |||||
**options) | |||||
self.backend = url or backend or self.app.conf.CELERY_CACHE_BACKEND | |||||
if self.backend: | |||||
self.backend, _, servers = self.backend.partition('://') | |||||
self.servers = servers.rstrip('/').split(';') | |||||
self.expires = self.prepare_expires(expires, type=int) | |||||
try: | |||||
self.Client, self.key_t = backends[self.backend]() | |||||
except KeyError: | |||||
raise ImproperlyConfigured(UNKNOWN_BACKEND.format( | |||||
self.backend, ', '.join(backends))) | |||||
self._encode_prefixes() # rencode the keyprefixes | |||||
def get(self, key): | |||||
return self.client.get(key) | |||||
def mget(self, keys): | |||||
return self.client.get_multi(keys) | |||||
def set(self, key, value): | |||||
return self.client.set(key, value, self.expires) | |||||
def delete(self, key): | |||||
return self.client.delete(key) | |||||
def _apply_chord_incr(self, header, partial_args, group_id, body, **opts): | |||||
self.client.set(self.get_key_for_chord(group_id), 0, time=86400) | |||||
return super(CacheBackend, self)._apply_chord_incr( | |||||
header, partial_args, group_id, body, **opts | |||||
) | |||||
def incr(self, key): | |||||
return self.client.incr(key) | |||||
@cached_property | |||||
def client(self): | |||||
return self.Client(self.servers, **self.options) | |||||
def __reduce__(self, args=(), kwargs={}): | |||||
servers = ';'.join(self.servers) | |||||
backend = '{0}://{1}/'.format(self.backend, servers) | |||||
kwargs.update( | |||||
dict(backend=backend, | |||||
expires=self.expires, | |||||
options=self.options)) | |||||
return super(CacheBackend, self).__reduce__(args, kwargs) | |||||
def as_uri(self, *args, **kwargs): | |||||
"""Return the backend as an URI. | |||||
This properly handles the case of multiple servers. | |||||
""" | |||||
servers = ';'.join(self.servers) | |||||
return '{0}://{1}/'.format(self.backend, servers) |
# -* coding: utf-8 -*- | |||||
""" | |||||
celery.backends.cassandra | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Apache Cassandra result store backend. | |||||
""" | |||||
from __future__ import absolute_import | |||||
try: # pragma: no cover | |||||
import pycassa | |||||
from thrift import Thrift | |||||
C = pycassa.cassandra.ttypes | |||||
except ImportError: # pragma: no cover | |||||
pycassa = None # noqa | |||||
import socket | |||||
import time | |||||
from celery import states | |||||
from celery.exceptions import ImproperlyConfigured | |||||
from celery.five import monotonic | |||||
from celery.utils.log import get_logger | |||||
from celery.utils.timeutils import maybe_timedelta, timedelta_seconds | |||||
from .base import BaseBackend | |||||
__all__ = ['CassandraBackend'] | |||||
logger = get_logger(__name__) | |||||
class CassandraBackend(BaseBackend): | |||||
"""Highly fault tolerant Cassandra backend. | |||||
.. attribute:: servers | |||||
List of Cassandra servers with format: ``hostname:port``. | |||||
:raises celery.exceptions.ImproperlyConfigured: if | |||||
module :mod:`pycassa` is not available. | |||||
""" | |||||
servers = [] | |||||
keyspace = None | |||||
column_family = None | |||||
detailed_mode = False | |||||
_retry_timeout = 300 | |||||
_retry_wait = 3 | |||||
supports_autoexpire = True | |||||
def __init__(self, servers=None, keyspace=None, column_family=None, | |||||
cassandra_options=None, detailed_mode=False, **kwargs): | |||||
"""Initialize Cassandra backend. | |||||
Raises :class:`celery.exceptions.ImproperlyConfigured` if | |||||
the :setting:`CASSANDRA_SERVERS` setting is not set. | |||||
""" | |||||
super(CassandraBackend, self).__init__(**kwargs) | |||||
self.expires = kwargs.get('expires') or maybe_timedelta( | |||||
self.app.conf.CELERY_TASK_RESULT_EXPIRES) | |||||
if not pycassa: | |||||
raise ImproperlyConfigured( | |||||
'You need to install the pycassa library to use the ' | |||||
'Cassandra backend. See https://github.com/pycassa/pycassa') | |||||
conf = self.app.conf | |||||
self.servers = (servers or | |||||
conf.get('CASSANDRA_SERVERS') or | |||||
self.servers) | |||||
self.keyspace = (keyspace or | |||||
conf.get('CASSANDRA_KEYSPACE') or | |||||
self.keyspace) | |||||
self.column_family = (column_family or | |||||
conf.get('CASSANDRA_COLUMN_FAMILY') or | |||||
self.column_family) | |||||
self.cassandra_options = dict(conf.get('CASSANDRA_OPTIONS') or {}, | |||||
**cassandra_options or {}) | |||||
self.detailed_mode = (detailed_mode or | |||||
conf.get('CASSANDRA_DETAILED_MODE') or | |||||
self.detailed_mode) | |||||
read_cons = conf.get('CASSANDRA_READ_CONSISTENCY') or 'LOCAL_QUORUM' | |||||
write_cons = conf.get('CASSANDRA_WRITE_CONSISTENCY') or 'LOCAL_QUORUM' | |||||
try: | |||||
self.read_consistency = getattr(pycassa.ConsistencyLevel, | |||||
read_cons) | |||||
except AttributeError: | |||||
self.read_consistency = pycassa.ConsistencyLevel.LOCAL_QUORUM | |||||
try: | |||||
self.write_consistency = getattr(pycassa.ConsistencyLevel, | |||||
write_cons) | |||||
except AttributeError: | |||||
self.write_consistency = pycassa.ConsistencyLevel.LOCAL_QUORUM | |||||
if not self.servers or not self.keyspace or not self.column_family: | |||||
raise ImproperlyConfigured( | |||||
'Cassandra backend not configured.') | |||||
self._column_family = None | |||||
def _retry_on_error(self, fun, *args, **kwargs): | |||||
ts = monotonic() + self._retry_timeout | |||||
while 1: | |||||
try: | |||||
return fun(*args, **kwargs) | |||||
except (pycassa.InvalidRequestException, | |||||
pycassa.TimedOutException, | |||||
pycassa.UnavailableException, | |||||
pycassa.AllServersUnavailable, | |||||
socket.error, | |||||
socket.timeout, | |||||
Thrift.TException) as exc: | |||||
if monotonic() > ts: | |||||
raise | |||||
logger.warning('Cassandra error: %r. Retrying...', exc) | |||||
time.sleep(self._retry_wait) | |||||
def _get_column_family(self): | |||||
if self._column_family is None: | |||||
conn = pycassa.ConnectionPool(self.keyspace, | |||||
server_list=self.servers, | |||||
**self.cassandra_options) | |||||
self._column_family = pycassa.ColumnFamily( | |||||
conn, self.column_family, | |||||
read_consistency_level=self.read_consistency, | |||||
write_consistency_level=self.write_consistency, | |||||
) | |||||
return self._column_family | |||||
def process_cleanup(self): | |||||
if self._column_family is not None: | |||||
self._column_family = None | |||||
def _store_result(self, task_id, result, status, | |||||
traceback=None, request=None, **kwargs): | |||||
"""Store return value and status of an executed task.""" | |||||
def _do_store(): | |||||
cf = self._get_column_family() | |||||
date_done = self.app.now() | |||||
meta = {'status': status, | |||||
'date_done': date_done.strftime('%Y-%m-%dT%H:%M:%SZ'), | |||||
'traceback': self.encode(traceback), | |||||
'result': self.encode(result), | |||||
'children': self.encode( | |||||
self.current_task_children(request), | |||||
)} | |||||
if self.detailed_mode: | |||||
cf.insert(task_id, {date_done: self.encode(meta)}, | |||||
ttl=self.expires and timedelta_seconds(self.expires)) | |||||
else: | |||||
cf.insert(task_id, meta, | |||||
ttl=self.expires and timedelta_seconds(self.expires)) | |||||
return self._retry_on_error(_do_store) | |||||
def as_uri(self, include_password=True): | |||||
return 'cassandra://' | |||||
def _get_task_meta_for(self, task_id): | |||||
"""Get task metadata for a task by id.""" | |||||
def _do_get(): | |||||
cf = self._get_column_family() | |||||
try: | |||||
if self.detailed_mode: | |||||
row = cf.get(task_id, column_reversed=True, column_count=1) | |||||
obj = self.decode(list(row.values())[0]) | |||||
else: | |||||
obj = cf.get(task_id) | |||||
meta = { | |||||
'task_id': task_id, | |||||
'status': obj['status'], | |||||
'result': self.decode(obj['result']), | |||||
'date_done': obj['date_done'], | |||||
'traceback': self.decode(obj['traceback']), | |||||
'children': self.decode(obj['children']), | |||||
} | |||||
except (KeyError, pycassa.NotFoundException): | |||||
meta = {'status': states.PENDING, 'result': None} | |||||
return meta | |||||
return self._retry_on_error(_do_get) | |||||
def __reduce__(self, args=(), kwargs={}): | |||||
kwargs.update( | |||||
dict(servers=self.servers, | |||||
keyspace=self.keyspace, | |||||
column_family=self.column_family, | |||||
cassandra_options=self.cassandra_options)) | |||||
return super(CassandraBackend, self).__reduce__(args, kwargs) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.couchbase | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
CouchBase result store backend. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import logging | |||||
try: | |||||
from couchbase import Couchbase | |||||
from couchbase.connection import Connection | |||||
from couchbase.exceptions import NotFoundError | |||||
except ImportError: | |||||
Couchbase = Connection = NotFoundError = None # noqa | |||||
from kombu.utils.url import _parse_url | |||||
from celery.exceptions import ImproperlyConfigured | |||||
from celery.utils.timeutils import maybe_timedelta | |||||
from .base import KeyValueStoreBackend | |||||
__all__ = ['CouchBaseBackend'] | |||||
class CouchBaseBackend(KeyValueStoreBackend): | |||||
"""CouchBase backend. | |||||
:raises celery.exceptions.ImproperlyConfigured: if | |||||
module :mod:`couchbase` is not available. | |||||
""" | |||||
bucket = 'default' | |||||
host = 'localhost' | |||||
port = 8091 | |||||
username = None | |||||
password = None | |||||
quiet = False | |||||
conncache = None | |||||
unlock_gil = True | |||||
timeout = 2.5 | |||||
transcoder = None | |||||
def __init__(self, url=None, *args, **kwargs): | |||||
super(CouchBaseBackend, self).__init__(*args, **kwargs) | |||||
self.url = url | |||||
self.expires = kwargs.get('expires') or maybe_timedelta( | |||||
self.app.conf.CELERY_TASK_RESULT_EXPIRES) | |||||
if Couchbase is None: | |||||
raise ImproperlyConfigured( | |||||
'You need to install the couchbase library to use the ' | |||||
'CouchBase backend.', | |||||
) | |||||
uhost = uport = uname = upass = ubucket = None | |||||
if url: | |||||
_, uhost, uport, uname, upass, ubucket, _ = _parse_url(url) | |||||
ubucket = ubucket.strip('/') if ubucket else None | |||||
config = self.app.conf.get('CELERY_COUCHBASE_BACKEND_SETTINGS', None) | |||||
if config is not None: | |||||
if not isinstance(config, dict): | |||||
raise ImproperlyConfigured( | |||||
'Couchbase backend settings should be grouped in a dict', | |||||
) | |||||
else: | |||||
config = {} | |||||
self.host = uhost or config.get('host', self.host) | |||||
self.port = int(uport or config.get('port', self.port)) | |||||
self.bucket = ubucket or config.get('bucket', self.bucket) | |||||
self.username = uname or config.get('username', self.username) | |||||
self.password = upass or config.get('password', self.password) | |||||
self._connection = None | |||||
def _get_connection(self): | |||||
"""Connect to the Couchbase server.""" | |||||
if self._connection is None: | |||||
kwargs = {'bucket': self.bucket, 'host': self.host} | |||||
if self.port: | |||||
kwargs.update({'port': self.port}) | |||||
if self.username: | |||||
kwargs.update({'username': self.username}) | |||||
if self.password: | |||||
kwargs.update({'password': self.password}) | |||||
logging.debug('couchbase settings %r', kwargs) | |||||
self._connection = Connection(**kwargs) | |||||
return self._connection | |||||
@property | |||||
def connection(self): | |||||
return self._get_connection() | |||||
def get(self, key): | |||||
try: | |||||
return self.connection.get(key).value | |||||
except NotFoundError: | |||||
return None | |||||
def set(self, key, value): | |||||
self.connection.set(key, value) | |||||
def mget(self, keys): | |||||
return [self.get(key) for key in keys] | |||||
def delete(self, key): | |||||
self.connection.delete(key) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.database | |||||
~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
SQLAlchemy result store backend. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import logging | |||||
from contextlib import contextmanager | |||||
from functools import wraps | |||||
from celery import states | |||||
from celery.backends.base import BaseBackend | |||||
from celery.exceptions import ImproperlyConfigured | |||||
from celery.five import range | |||||
from celery.utils.timeutils import maybe_timedelta | |||||
from .models import Task | |||||
from .models import TaskSet | |||||
from .session import SessionManager | |||||
logger = logging.getLogger(__name__) | |||||
__all__ = ['DatabaseBackend'] | |||||
def _sqlalchemy_installed(): | |||||
try: | |||||
import sqlalchemy | |||||
except ImportError: | |||||
raise ImproperlyConfigured( | |||||
'The database result backend requires SQLAlchemy to be installed.' | |||||
'See http://pypi.python.org/pypi/SQLAlchemy') | |||||
return sqlalchemy | |||||
_sqlalchemy_installed() | |||||
from sqlalchemy.exc import DatabaseError, InvalidRequestError # noqa | |||||
from sqlalchemy.orm.exc import StaleDataError # noqa | |||||
@contextmanager | |||||
def session_cleanup(session): | |||||
try: | |||||
yield | |||||
except Exception: | |||||
session.rollback() | |||||
raise | |||||
finally: | |||||
session.close() | |||||
def retry(fun): | |||||
@wraps(fun) | |||||
def _inner(*args, **kwargs): | |||||
max_retries = kwargs.pop('max_retries', 3) | |||||
for retries in range(max_retries): | |||||
try: | |||||
return fun(*args, **kwargs) | |||||
except (DatabaseError, InvalidRequestError, StaleDataError): | |||||
logger.warning( | |||||
"Failed operation %s. Retrying %s more times.", | |||||
fun.__name__, max_retries - retries - 1, | |||||
exc_info=True, | |||||
) | |||||
if retries + 1 >= max_retries: | |||||
raise | |||||
return _inner | |||||
class DatabaseBackend(BaseBackend): | |||||
"""The database result backend.""" | |||||
# ResultSet.iterate should sleep this much between each pool, | |||||
# to not bombard the database with queries. | |||||
subpolling_interval = 0.5 | |||||
def __init__(self, dburi=None, expires=None, | |||||
engine_options=None, url=None, **kwargs): | |||||
# The `url` argument was added later and is used by | |||||
# the app to set backend by url (celery.backends.get_backend_by_url) | |||||
super(DatabaseBackend, self).__init__(**kwargs) | |||||
conf = self.app.conf | |||||
self.expires = maybe_timedelta(self.prepare_expires(expires)) | |||||
self.url = url or dburi or conf.CELERY_RESULT_DBURI | |||||
self.engine_options = dict( | |||||
engine_options or {}, | |||||
**conf.CELERY_RESULT_ENGINE_OPTIONS or {}) | |||||
self.short_lived_sessions = kwargs.get( | |||||
'short_lived_sessions', | |||||
conf.CELERY_RESULT_DB_SHORT_LIVED_SESSIONS, | |||||
) | |||||
tablenames = conf.CELERY_RESULT_DB_TABLENAMES or {} | |||||
Task.__table__.name = tablenames.get('task', 'celery_taskmeta') | |||||
TaskSet.__table__.name = tablenames.get('group', 'celery_tasksetmeta') | |||||
if not self.url: | |||||
raise ImproperlyConfigured( | |||||
'Missing connection string! Do you have ' | |||||
'CELERY_RESULT_DBURI set to a real value?') | |||||
def ResultSession(self, session_manager=SessionManager()): | |||||
return session_manager.session_factory( | |||||
dburi=self.url, | |||||
short_lived_sessions=self.short_lived_sessions, | |||||
**self.engine_options | |||||
) | |||||
@retry | |||||
def _store_result(self, task_id, result, status, | |||||
traceback=None, max_retries=3, **kwargs): | |||||
"""Store return value and status of an executed task.""" | |||||
session = self.ResultSession() | |||||
with session_cleanup(session): | |||||
task = list(session.query(Task).filter(Task.task_id == task_id)) | |||||
task = task and task[0] | |||||
if not task: | |||||
task = Task(task_id) | |||||
session.add(task) | |||||
session.flush() | |||||
task.result = result | |||||
task.status = status | |||||
task.traceback = traceback | |||||
session.commit() | |||||
return result | |||||
@retry | |||||
def _get_task_meta_for(self, task_id): | |||||
"""Get task metadata for a task by id.""" | |||||
session = self.ResultSession() | |||||
with session_cleanup(session): | |||||
task = list(session.query(Task).filter(Task.task_id == task_id)) | |||||
task = task and task[0] | |||||
if not task: | |||||
task = Task(task_id) | |||||
task.status = states.PENDING | |||||
task.result = None | |||||
return self.meta_from_decoded(task.to_dict()) | |||||
@retry | |||||
def _save_group(self, group_id, result): | |||||
"""Store the result of an executed group.""" | |||||
session = self.ResultSession() | |||||
with session_cleanup(session): | |||||
group = TaskSet(group_id, result) | |||||
session.add(group) | |||||
session.flush() | |||||
session.commit() | |||||
return result | |||||
@retry | |||||
def _restore_group(self, group_id): | |||||
"""Get metadata for group by id.""" | |||||
session = self.ResultSession() | |||||
with session_cleanup(session): | |||||
group = session.query(TaskSet).filter( | |||||
TaskSet.taskset_id == group_id).first() | |||||
if group: | |||||
return group.to_dict() | |||||
@retry | |||||
def _delete_group(self, group_id): | |||||
"""Delete metadata for group by id.""" | |||||
session = self.ResultSession() | |||||
with session_cleanup(session): | |||||
session.query(TaskSet).filter( | |||||
TaskSet.taskset_id == group_id).delete() | |||||
session.flush() | |||||
session.commit() | |||||
@retry | |||||
def _forget(self, task_id): | |||||
"""Forget about result.""" | |||||
session = self.ResultSession() | |||||
with session_cleanup(session): | |||||
session.query(Task).filter(Task.task_id == task_id).delete() | |||||
session.commit() | |||||
def cleanup(self): | |||||
"""Delete expired metadata.""" | |||||
session = self.ResultSession() | |||||
expires = self.expires | |||||
now = self.app.now() | |||||
with session_cleanup(session): | |||||
session.query(Task).filter( | |||||
Task.date_done < (now - expires)).delete() | |||||
session.query(TaskSet).filter( | |||||
TaskSet.date_done < (now - expires)).delete() | |||||
session.commit() | |||||
def __reduce__(self, args=(), kwargs={}): | |||||
kwargs.update( | |||||
dict(dburi=self.url, | |||||
expires=self.expires, | |||||
engine_options=self.engine_options)) | |||||
return super(DatabaseBackend, self).__reduce__(args, kwargs) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.database.models | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Database tables for the SQLAlchemy result store backend. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from datetime import datetime | |||||
import sqlalchemy as sa | |||||
from sqlalchemy.types import PickleType | |||||
from celery import states | |||||
from .session import ResultModelBase | |||||
__all__ = ['Task', 'TaskSet'] | |||||
class Task(ResultModelBase): | |||||
"""Task result/status.""" | |||||
__tablename__ = 'celery_taskmeta' | |||||
__table_args__ = {'sqlite_autoincrement': True} | |||||
id = sa.Column(sa.Integer, sa.Sequence('task_id_sequence'), | |||||
primary_key=True, | |||||
autoincrement=True) | |||||
task_id = sa.Column(sa.String(255), unique=True) | |||||
status = sa.Column(sa.String(50), default=states.PENDING) | |||||
result = sa.Column(PickleType, nullable=True) | |||||
date_done = sa.Column(sa.DateTime, default=datetime.utcnow, | |||||
onupdate=datetime.utcnow, nullable=True) | |||||
traceback = sa.Column(sa.Text, nullable=True) | |||||
def __init__(self, task_id): | |||||
self.task_id = task_id | |||||
def to_dict(self): | |||||
return {'task_id': self.task_id, | |||||
'status': self.status, | |||||
'result': self.result, | |||||
'traceback': self.traceback, | |||||
'date_done': self.date_done} | |||||
def __repr__(self): | |||||
return '<Task {0.task_id} state: {0.status}>'.format(self) | |||||
class TaskSet(ResultModelBase): | |||||
"""TaskSet result""" | |||||
__tablename__ = 'celery_tasksetmeta' | |||||
__table_args__ = {'sqlite_autoincrement': True} | |||||
id = sa.Column(sa.Integer, sa.Sequence('taskset_id_sequence'), | |||||
autoincrement=True, primary_key=True) | |||||
taskset_id = sa.Column(sa.String(255), unique=True) | |||||
result = sa.Column(PickleType, nullable=True) | |||||
date_done = sa.Column(sa.DateTime, default=datetime.utcnow, | |||||
nullable=True) | |||||
def __init__(self, taskset_id, result): | |||||
self.taskset_id = taskset_id | |||||
self.result = result | |||||
def to_dict(self): | |||||
return {'taskset_id': self.taskset_id, | |||||
'result': self.result, | |||||
'date_done': self.date_done} | |||||
def __repr__(self): | |||||
return '<TaskSet: {0.taskset_id}>'.format(self) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.database.session | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
SQLAlchemy sessions. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from billiard.util import register_after_fork | |||||
from sqlalchemy import create_engine | |||||
from sqlalchemy.ext.declarative import declarative_base | |||||
from sqlalchemy.orm import sessionmaker | |||||
from sqlalchemy.pool import NullPool | |||||
ResultModelBase = declarative_base() | |||||
__all__ = ['SessionManager'] | |||||
class SessionManager(object): | |||||
def __init__(self): | |||||
self._engines = {} | |||||
self._sessions = {} | |||||
self.forked = False | |||||
self.prepared = False | |||||
register_after_fork(self, self._after_fork) | |||||
def _after_fork(self,): | |||||
self.forked = True | |||||
def get_engine(self, dburi, **kwargs): | |||||
if self.forked: | |||||
try: | |||||
return self._engines[dburi] | |||||
except KeyError: | |||||
engine = self._engines[dburi] = create_engine(dburi, **kwargs) | |||||
return engine | |||||
else: | |||||
kwargs['poolclass'] = NullPool | |||||
return create_engine(dburi, **kwargs) | |||||
def create_session(self, dburi, short_lived_sessions=False, **kwargs): | |||||
engine = self.get_engine(dburi, **kwargs) | |||||
if self.forked: | |||||
if short_lived_sessions or dburi not in self._sessions: | |||||
self._sessions[dburi] = sessionmaker(bind=engine) | |||||
return engine, self._sessions[dburi] | |||||
else: | |||||
return engine, sessionmaker(bind=engine) | |||||
def prepare_models(self, engine): | |||||
if not self.prepared: | |||||
ResultModelBase.metadata.create_all(engine) | |||||
self.prepared = True | |||||
def session_factory(self, dburi, **kwargs): | |||||
engine, session = self.create_session(dburi, **kwargs) | |||||
self.prepare_models(engine) | |||||
return session() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.mongodb | |||||
~~~~~~~~~~~~~~~~~~~~~~~ | |||||
MongoDB result store backend. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from datetime import datetime | |||||
from kombu.syn import detect_environment | |||||
from kombu.utils import cached_property | |||||
from kombu.utils.url import maybe_sanitize_url | |||||
from celery import states | |||||
from celery.exceptions import ImproperlyConfigured | |||||
from celery.five import items, string_t | |||||
from celery.utils.timeutils import maybe_timedelta | |||||
from .base import BaseBackend | |||||
try: | |||||
import pymongo | |||||
except ImportError: # pragma: no cover | |||||
pymongo = None # noqa | |||||
if pymongo: | |||||
try: | |||||
from bson.binary import Binary | |||||
except ImportError: # pragma: no cover | |||||
from pymongo.binary import Binary # noqa | |||||
else: # pragma: no cover | |||||
Binary = None # noqa | |||||
__all__ = ['MongoBackend'] | |||||
class MongoBackend(BaseBackend): | |||||
"""MongoDB result backend. | |||||
:raises celery.exceptions.ImproperlyConfigured: if | |||||
module :mod:`pymongo` is not available. | |||||
""" | |||||
host = 'localhost' | |||||
port = 27017 | |||||
user = None | |||||
password = None | |||||
database_name = 'celery' | |||||
taskmeta_collection = 'celery_taskmeta' | |||||
max_pool_size = 10 | |||||
options = None | |||||
supports_autoexpire = False | |||||
_connection = None | |||||
def __init__(self, app=None, url=None, **kwargs): | |||||
self.options = {} | |||||
super(MongoBackend, self).__init__(app, **kwargs) | |||||
self.expires = kwargs.get('expires') or maybe_timedelta( | |||||
self.app.conf.CELERY_TASK_RESULT_EXPIRES) | |||||
if not pymongo: | |||||
raise ImproperlyConfigured( | |||||
'You need to install the pymongo library to use the ' | |||||
'MongoDB backend.') | |||||
config = self.app.conf.get('CELERY_MONGODB_BACKEND_SETTINGS') | |||||
if config is not None: | |||||
if not isinstance(config, dict): | |||||
raise ImproperlyConfigured( | |||||
'MongoDB backend settings should be grouped in a dict') | |||||
config = dict(config) # do not modify original | |||||
self.host = config.pop('host', self.host) | |||||
self.port = int(config.pop('port', self.port)) | |||||
self.user = config.pop('user', self.user) | |||||
self.password = config.pop('password', self.password) | |||||
self.database_name = config.pop('database', self.database_name) | |||||
self.taskmeta_collection = config.pop( | |||||
'taskmeta_collection', self.taskmeta_collection, | |||||
) | |||||
self.options = dict(config, **config.pop('options', None) or {}) | |||||
# Set option defaults | |||||
for key, value in items(self._prepare_client_options()): | |||||
self.options.setdefault(key, value) | |||||
self.url = url | |||||
if self.url: | |||||
# Specifying backend as an URL | |||||
self.host = self.url | |||||
def _prepare_client_options(self): | |||||
if pymongo.version_tuple >= (3, ): | |||||
return {'maxPoolSize': self.max_pool_size} | |||||
else: # pragma: no cover | |||||
options = { | |||||
'max_pool_size': self.max_pool_size, | |||||
'auto_start_request': False | |||||
} | |||||
if detect_environment() != 'default': | |||||
options['use_greenlets'] = True | |||||
return options | |||||
def _get_connection(self): | |||||
"""Connect to the MongoDB server.""" | |||||
if self._connection is None: | |||||
from pymongo import MongoClient | |||||
# The first pymongo.Connection() argument (host) can be | |||||
# a list of ['host:port'] elements or a mongodb connection | |||||
# URI. If this is the case, don't use self.port | |||||
# but let pymongo get the port(s) from the URI instead. | |||||
# This enables the use of replica sets and sharding. | |||||
# See pymongo.Connection() for more info. | |||||
url = self.host | |||||
if isinstance(url, string_t) \ | |||||
and not url.startswith('mongodb://'): | |||||
url = 'mongodb://{0}:{1}'.format(url, self.port) | |||||
if url == 'mongodb://': | |||||
url = url + 'localhost' | |||||
self._connection = MongoClient(host=url, **self.options) | |||||
return self._connection | |||||
def process_cleanup(self): | |||||
if self._connection is not None: | |||||
# MongoDB connection will be closed automatically when object | |||||
# goes out of scope | |||||
del(self.collection) | |||||
del(self.database) | |||||
self._connection = None | |||||
def _store_result(self, task_id, result, status, | |||||
traceback=None, request=None, **kwargs): | |||||
"""Store return value and status of an executed task.""" | |||||
meta = {'_id': task_id, | |||||
'status': status, | |||||
'result': Binary(self.encode(result)), | |||||
'date_done': datetime.utcnow(), | |||||
'traceback': Binary(self.encode(traceback)), | |||||
'children': Binary(self.encode( | |||||
self.current_task_children(request), | |||||
))} | |||||
self.collection.save(meta) | |||||
return result | |||||
def _get_task_meta_for(self, task_id): | |||||
"""Get task metadata for a task by id.""" | |||||
obj = self.collection.find_one({'_id': task_id}) | |||||
if not obj: | |||||
return {'status': states.PENDING, 'result': None} | |||||
meta = { | |||||
'task_id': obj['_id'], | |||||
'status': obj['status'], | |||||
'result': self.decode(obj['result']), | |||||
'date_done': obj['date_done'], | |||||
'traceback': self.decode(obj['traceback']), | |||||
'children': self.decode(obj['children']), | |||||
} | |||||
return meta | |||||
def _save_group(self, group_id, result): | |||||
"""Save the group result.""" | |||||
meta = {'_id': group_id, | |||||
'result': Binary(self.encode(result)), | |||||
'date_done': datetime.utcnow()} | |||||
self.collection.save(meta) | |||||
return result | |||||
def _restore_group(self, group_id): | |||||
"""Get the result for a group by id.""" | |||||
obj = self.collection.find_one({'_id': group_id}) | |||||
if not obj: | |||||
return | |||||
meta = { | |||||
'task_id': obj['_id'], | |||||
'result': self.decode(obj['result']), | |||||
'date_done': obj['date_done'], | |||||
} | |||||
return meta | |||||
def _delete_group(self, group_id): | |||||
"""Delete a group by id.""" | |||||
self.collection.remove({'_id': group_id}) | |||||
def _forget(self, task_id): | |||||
"""Remove result from MongoDB. | |||||
:raises celery.exceptions.OperationsError: | |||||
if the task_id could not be removed. | |||||
""" | |||||
# By using safe=True, this will wait until it receives a response from | |||||
# the server. Likewise, it will raise an OperationsError if the | |||||
# response was unable to be completed. | |||||
self.collection.remove({'_id': task_id}) | |||||
def cleanup(self): | |||||
"""Delete expired metadata.""" | |||||
self.collection.remove( | |||||
{'date_done': {'$lt': self.app.now() - self.expires}}, | |||||
) | |||||
def __reduce__(self, args=(), kwargs={}): | |||||
return super(MongoBackend, self).__reduce__( | |||||
args, dict(kwargs, expires=self.expires, url=self.url), | |||||
) | |||||
def _get_database(self): | |||||
conn = self._get_connection() | |||||
db = conn[self.database_name] | |||||
if self.user and self.password: | |||||
if not db.authenticate(self.user, | |||||
self.password): | |||||
raise ImproperlyConfigured( | |||||
'Invalid MongoDB username or password.') | |||||
return db | |||||
@cached_property | |||||
def database(self): | |||||
"""Get database from MongoDB connection and perform authentication | |||||
if necessary.""" | |||||
return self._get_database() | |||||
@cached_property | |||||
def collection(self): | |||||
"""Get the metadata task collection.""" | |||||
collection = self.database[self.taskmeta_collection] | |||||
# Ensure an index on date_done is there, if not process the index | |||||
# in the background. Once completed cleanup will be much faster | |||||
collection.ensure_index('date_done', background='true') | |||||
return collection | |||||
def as_uri(self, include_password=False): | |||||
"""Return the backend as an URI. | |||||
:keyword include_password: Censor passwords. | |||||
""" | |||||
if not self.url: | |||||
return 'mongodb://' | |||||
if include_password: | |||||
return self.url | |||||
if ',' not in self.url: | |||||
return maybe_sanitize_url(self.url) | |||||
uri1, remainder = self.url.split(',', 1) | |||||
return ','.join([maybe_sanitize_url(uri1), remainder]) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.redis | |||||
~~~~~~~~~~~~~~~~~~~~~ | |||||
Redis result store backend. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from functools import partial | |||||
from kombu.utils import cached_property, retry_over_time | |||||
from kombu.utils.url import _parse_url | |||||
from celery import states | |||||
from celery.canvas import maybe_signature | |||||
from celery.exceptions import ChordError, ImproperlyConfigured | |||||
from celery.five import string_t | |||||
from celery.utils import deprecated_property, strtobool | |||||
from celery.utils.functional import dictfilter | |||||
from celery.utils.log import get_logger | |||||
from celery.utils.timeutils import humanize_seconds | |||||
from .base import KeyValueStoreBackend | |||||
try: | |||||
import redis | |||||
from redis.exceptions import ConnectionError | |||||
from kombu.transport.redis import get_redis_error_classes | |||||
except ImportError: # pragma: no cover | |||||
redis = None # noqa | |||||
ConnectionError = None # noqa | |||||
get_redis_error_classes = None # noqa | |||||
__all__ = ['RedisBackend'] | |||||
REDIS_MISSING = """\ | |||||
You need to install the redis library in order to use \ | |||||
the Redis result store backend.""" | |||||
logger = get_logger(__name__) | |||||
error = logger.error | |||||
class RedisBackend(KeyValueStoreBackend): | |||||
"""Redis task result store.""" | |||||
#: redis-py client module. | |||||
redis = redis | |||||
#: Maximium number of connections in the pool. | |||||
max_connections = None | |||||
supports_autoexpire = True | |||||
supports_native_join = True | |||||
implements_incr = True | |||||
def __init__(self, host=None, port=None, db=None, password=None, | |||||
expires=None, max_connections=None, url=None, | |||||
connection_pool=None, new_join=False, **kwargs): | |||||
super(RedisBackend, self).__init__(**kwargs) | |||||
conf = self.app.conf | |||||
if self.redis is None: | |||||
raise ImproperlyConfigured(REDIS_MISSING) | |||||
self._client_capabilities = self._detect_client_capabilities() | |||||
# For compatibility with the old REDIS_* configuration keys. | |||||
def _get(key): | |||||
for prefix in 'CELERY_REDIS_{0}', 'REDIS_{0}': | |||||
try: | |||||
return conf[prefix.format(key)] | |||||
except KeyError: | |||||
pass | |||||
if host and '://' in host: | |||||
url = host | |||||
host = None | |||||
self.max_connections = ( | |||||
max_connections or _get('MAX_CONNECTIONS') or self.max_connections | |||||
) | |||||
self._ConnectionPool = connection_pool | |||||
self.connparams = { | |||||
'host': _get('HOST') or 'localhost', | |||||
'port': _get('PORT') or 6379, | |||||
'db': _get('DB') or 0, | |||||
'password': _get('PASSWORD'), | |||||
'max_connections': self.max_connections, | |||||
} | |||||
if url: | |||||
self.connparams = self._params_from_url(url, self.connparams) | |||||
self.url = url | |||||
self.expires = self.prepare_expires(expires, type=int) | |||||
try: | |||||
new_join = strtobool(self.connparams.pop('new_join')) | |||||
except KeyError: | |||||
pass | |||||
if new_join: | |||||
self.apply_chord = self._new_chord_apply | |||||
self.on_chord_part_return = self._new_chord_return | |||||
self.connection_errors, self.channel_errors = ( | |||||
get_redis_error_classes() if get_redis_error_classes | |||||
else ((), ())) | |||||
def _params_from_url(self, url, defaults): | |||||
scheme, host, port, user, password, path, query = _parse_url(url) | |||||
connparams = dict( | |||||
defaults, **dictfilter({ | |||||
'host': host, 'port': port, 'password': password, | |||||
'db': query.pop('virtual_host', None)}) | |||||
) | |||||
if scheme == 'socket': | |||||
# use 'path' as path to the socket… in this case | |||||
# the database number should be given in 'query' | |||||
connparams.update({ | |||||
'connection_class': self.redis.UnixDomainSocketConnection, | |||||
'path': '/' + path, | |||||
}) | |||||
# host+port are invalid options when using this connection type. | |||||
connparams.pop('host', None) | |||||
connparams.pop('port', None) | |||||
else: | |||||
connparams['db'] = path | |||||
# db may be string and start with / like in kombu. | |||||
db = connparams.get('db') or 0 | |||||
db = db.strip('/') if isinstance(db, string_t) else db | |||||
connparams['db'] = int(db) | |||||
# Query parameters override other parameters | |||||
connparams.update(query) | |||||
return connparams | |||||
def get(self, key): | |||||
return self.client.get(key) | |||||
def mget(self, keys): | |||||
return self.client.mget(keys) | |||||
def ensure(self, fun, args, **policy): | |||||
retry_policy = dict(self.retry_policy, **policy) | |||||
max_retries = retry_policy.get('max_retries') | |||||
return retry_over_time( | |||||
fun, self.connection_errors, args, {}, | |||||
partial(self.on_connection_error, max_retries), | |||||
**retry_policy | |||||
) | |||||
def on_connection_error(self, max_retries, exc, intervals, retries): | |||||
tts = next(intervals) | |||||
error('Connection to Redis lost: Retry (%s/%s) %s.', | |||||
retries, max_retries or 'Inf', | |||||
humanize_seconds(tts, 'in ')) | |||||
return tts | |||||
def set(self, key, value, **retry_policy): | |||||
return self.ensure(self._set, (key, value), **retry_policy) | |||||
def _set(self, key, value): | |||||
with self.client.pipeline() as pipe: | |||||
if self.expires: | |||||
pipe.setex(key, value, self.expires) | |||||
else: | |||||
pipe.set(key, value) | |||||
pipe.publish(key, value) | |||||
pipe.execute() | |||||
def delete(self, key): | |||||
self.client.delete(key) | |||||
def incr(self, key): | |||||
return self.client.incr(key) | |||||
def expire(self, key, value): | |||||
return self.client.expire(key, value) | |||||
def _unpack_chord_result(self, tup, decode, | |||||
EXCEPTION_STATES=states.EXCEPTION_STATES, | |||||
PROPAGATE_STATES=states.PROPAGATE_STATES): | |||||
_, tid, state, retval = decode(tup) | |||||
if state in EXCEPTION_STATES: | |||||
retval = self.exception_to_python(retval) | |||||
if state in PROPAGATE_STATES: | |||||
raise ChordError('Dependency {0} raised {1!r}'.format(tid, retval)) | |||||
return retval | |||||
def _new_chord_apply(self, header, partial_args, group_id, body, | |||||
result=None, **options): | |||||
# avoids saving the group in the redis db. | |||||
return header(*partial_args, task_id=group_id) | |||||
def _new_chord_return(self, task, state, result, propagate=None, | |||||
PROPAGATE_STATES=states.PROPAGATE_STATES): | |||||
app = self.app | |||||
if propagate is None: | |||||
propagate = self.app.conf.CELERY_CHORD_PROPAGATES | |||||
request = task.request | |||||
tid, gid = request.id, request.group | |||||
if not gid or not tid: | |||||
return | |||||
client = self.client | |||||
jkey = self.get_key_for_group(gid, '.j') | |||||
result = self.encode_result(result, state) | |||||
with client.pipeline() as pipe: | |||||
_, readycount, _ = pipe \ | |||||
.rpush(jkey, self.encode([1, tid, state, result])) \ | |||||
.llen(jkey) \ | |||||
.expire(jkey, 86400) \ | |||||
.execute() | |||||
try: | |||||
callback = maybe_signature(request.chord, app=app) | |||||
total = callback['chord_size'] | |||||
if readycount == total: | |||||
decode, unpack = self.decode, self._unpack_chord_result | |||||
with client.pipeline() as pipe: | |||||
resl, _, = pipe \ | |||||
.lrange(jkey, 0, total) \ | |||||
.delete(jkey) \ | |||||
.execute() | |||||
try: | |||||
callback.delay([unpack(tup, decode) for tup in resl]) | |||||
except Exception as exc: | |||||
error('Chord callback for %r raised: %r', | |||||
request.group, exc, exc_info=1) | |||||
return self.chord_error_from_stack( | |||||
callback, | |||||
ChordError('Callback error: {0!r}'.format(exc)), | |||||
) | |||||
except ChordError as exc: | |||||
error('Chord %r raised: %r', request.group, exc, exc_info=1) | |||||
return self.chord_error_from_stack(callback, exc) | |||||
except Exception as exc: | |||||
error('Chord %r raised: %r', request.group, exc, exc_info=1) | |||||
return self.chord_error_from_stack( | |||||
callback, ChordError('Join error: {0!r}'.format(exc)), | |||||
) | |||||
def _detect_client_capabilities(self, socket_connect_timeout=False): | |||||
if self.redis.VERSION < (2, 4, 4): | |||||
raise ImproperlyConfigured( | |||||
'Redis backend requires redis-py versions 2.4.4 or later. ' | |||||
'You have {0.__version__}'.format(redis)) | |||||
if self.redis.VERSION >= (2, 10): | |||||
socket_connect_timeout = True | |||||
return {'socket_connect_timeout': socket_connect_timeout} | |||||
def _create_client(self, socket_timeout=None, socket_connect_timeout=None, | |||||
**params): | |||||
return self._new_redis_client( | |||||
socket_timeout=socket_timeout and float(socket_timeout), | |||||
socket_connect_timeout=socket_connect_timeout and float( | |||||
socket_connect_timeout), **params | |||||
) | |||||
def _new_redis_client(self, **params): | |||||
if not self._client_capabilities['socket_connect_timeout']: | |||||
params.pop('socket_connect_timeout', None) | |||||
return self.redis.Redis(connection_pool=self.ConnectionPool(**params)) | |||||
@property | |||||
def ConnectionPool(self): | |||||
if self._ConnectionPool is None: | |||||
self._ConnectionPool = self.redis.ConnectionPool | |||||
return self._ConnectionPool | |||||
@cached_property | |||||
def client(self): | |||||
return self._create_client(**self.connparams) | |||||
def __reduce__(self, args=(), kwargs={}): | |||||
return super(RedisBackend, self).__reduce__( | |||||
(self.url, ), {'expires': self.expires}, | |||||
) | |||||
@deprecated_property(3.2, 3.3) | |||||
def host(self): | |||||
return self.connparams['host'] | |||||
@deprecated_property(3.2, 3.3) | |||||
def port(self): | |||||
return self.connparams['port'] | |||||
@deprecated_property(3.2, 3.3) | |||||
def db(self): | |||||
return self.connparams['db'] | |||||
@deprecated_property(3.2, 3.3) | |||||
def password(self): | |||||
return self.connparams['password'] |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.backends.rpc | |||||
~~~~~~~~~~~~~~~~~~~ | |||||
RPC-style result backend, using reply-to and one queue per client. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from kombu import Consumer, Exchange | |||||
from kombu.common import maybe_declare | |||||
from kombu.utils import cached_property | |||||
from celery import current_task | |||||
from celery.backends import amqp | |||||
__all__ = ['RPCBackend'] | |||||
class RPCBackend(amqp.AMQPBackend): | |||||
persistent = False | |||||
class Consumer(Consumer): | |||||
auto_declare = False | |||||
def _create_exchange(self, name, type='direct', delivery_mode=2): | |||||
# uses direct to queue routing (anon exchange). | |||||
return Exchange(None) | |||||
def on_task_call(self, producer, task_id): | |||||
maybe_declare(self.binding(producer.channel), retry=True) | |||||
def _create_binding(self, task_id): | |||||
return self.binding | |||||
def _many_bindings(self, ids): | |||||
return [self.binding] | |||||
def rkey(self, task_id): | |||||
return task_id | |||||
def destination_for(self, task_id, request): | |||||
# Request is a new argument for backends, so must still support | |||||
# old code that rely on current_task | |||||
try: | |||||
request = request or current_task.request | |||||
except AttributeError: | |||||
raise RuntimeError( | |||||
'RPC backend missing task request for {0!r}'.format(task_id), | |||||
) | |||||
return request.reply_to, request.correlation_id or task_id | |||||
def on_reply_declare(self, task_id): | |||||
pass | |||||
def as_uri(self, include_password=True): | |||||
return 'rpc://' | |||||
@property | |||||
def binding(self): | |||||
return self.Queue(self.oid, self.exchange, self.oid, | |||||
durable=False, auto_delete=False) | |||||
@cached_property | |||||
def oid(self): | |||||
return self.app.oid |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.beat | |||||
~~~~~~~~~~~ | |||||
The periodic task scheduler. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import errno | |||||
import os | |||||
import time | |||||
import shelve | |||||
import sys | |||||
import traceback | |||||
from threading import Event, Thread | |||||
from billiard import ensure_multiprocessing | |||||
from billiard.process import Process | |||||
from billiard.common import reset_signals | |||||
from kombu.utils import cached_property, reprcall | |||||
from kombu.utils.functional import maybe_evaluate | |||||
from . import __version__ | |||||
from . import platforms | |||||
from . import signals | |||||
from .five import items, reraise, values, monotonic | |||||
from .schedules import maybe_schedule, crontab | |||||
from .utils.imports import instantiate | |||||
from .utils.timeutils import humanize_seconds | |||||
from .utils.log import get_logger, iter_open_logger_fds | |||||
__all__ = ['SchedulingError', 'ScheduleEntry', 'Scheduler', | |||||
'PersistentScheduler', 'Service', 'EmbeddedService'] | |||||
logger = get_logger(__name__) | |||||
debug, info, error, warning = (logger.debug, logger.info, | |||||
logger.error, logger.warning) | |||||
DEFAULT_MAX_INTERVAL = 300 # 5 minutes | |||||
class SchedulingError(Exception): | |||||
"""An error occured while scheduling a task.""" | |||||
class ScheduleEntry(object): | |||||
"""An entry in the scheduler. | |||||
:keyword name: see :attr:`name`. | |||||
:keyword schedule: see :attr:`schedule`. | |||||
:keyword args: see :attr:`args`. | |||||
:keyword kwargs: see :attr:`kwargs`. | |||||
:keyword options: see :attr:`options`. | |||||
:keyword last_run_at: see :attr:`last_run_at`. | |||||
:keyword total_run_count: see :attr:`total_run_count`. | |||||
:keyword relative: Is the time relative to when the server starts? | |||||
""" | |||||
#: The task name | |||||
name = None | |||||
#: The schedule (run_every/crontab) | |||||
schedule = None | |||||
#: Positional arguments to apply. | |||||
args = None | |||||
#: Keyword arguments to apply. | |||||
kwargs = None | |||||
#: Task execution options. | |||||
options = None | |||||
#: The time and date of when this task was last scheduled. | |||||
last_run_at = None | |||||
#: Total number of times this task has been scheduled. | |||||
total_run_count = 0 | |||||
def __init__(self, name=None, task=None, last_run_at=None, | |||||
total_run_count=None, schedule=None, args=(), kwargs={}, | |||||
options={}, relative=False, app=None): | |||||
self.app = app | |||||
self.name = name | |||||
self.task = task | |||||
self.args = args | |||||
self.kwargs = kwargs | |||||
self.options = options | |||||
self.schedule = maybe_schedule(schedule, relative, app=self.app) | |||||
self.last_run_at = last_run_at or self._default_now() | |||||
self.total_run_count = total_run_count or 0 | |||||
def _default_now(self): | |||||
return self.schedule.now() if self.schedule else self.app.now() | |||||
def _next_instance(self, last_run_at=None): | |||||
"""Return a new instance of the same class, but with | |||||
its date and count fields updated.""" | |||||
return self.__class__(**dict( | |||||
self, | |||||
last_run_at=last_run_at or self._default_now(), | |||||
total_run_count=self.total_run_count + 1, | |||||
)) | |||||
__next__ = next = _next_instance # for 2to3 | |||||
def __reduce__(self): | |||||
return self.__class__, ( | |||||
self.name, self.task, self.last_run_at, self.total_run_count, | |||||
self.schedule, self.args, self.kwargs, self.options, | |||||
) | |||||
def update(self, other): | |||||
"""Update values from another entry. | |||||
Does only update "editable" fields (task, schedule, args, kwargs, | |||||
options). | |||||
""" | |||||
self.__dict__.update({'task': other.task, 'schedule': other.schedule, | |||||
'args': other.args, 'kwargs': other.kwargs, | |||||
'options': other.options}) | |||||
def is_due(self): | |||||
"""See :meth:`~celery.schedule.schedule.is_due`.""" | |||||
return self.schedule.is_due(self.last_run_at) | |||||
def __iter__(self): | |||||
return iter(items(vars(self))) | |||||
def __repr__(self): | |||||
return '<Entry: {0.name} {call} {0.schedule}'.format( | |||||
self, | |||||
call=reprcall(self.task, self.args or (), self.kwargs or {}), | |||||
) | |||||
class Scheduler(object): | |||||
"""Scheduler for periodic tasks. | |||||
The :program:`celery beat` program may instantiate this class | |||||
multiple times for introspection purposes, but then with the | |||||
``lazy`` argument set. It is important for subclasses to | |||||
be idempotent when this argument is set. | |||||
:keyword schedule: see :attr:`schedule`. | |||||
:keyword max_interval: see :attr:`max_interval`. | |||||
:keyword lazy: Do not set up the schedule. | |||||
""" | |||||
Entry = ScheduleEntry | |||||
#: The schedule dict/shelve. | |||||
schedule = None | |||||
#: Maximum time to sleep between re-checking the schedule. | |||||
max_interval = DEFAULT_MAX_INTERVAL | |||||
#: How often to sync the schedule (3 minutes by default) | |||||
sync_every = 3 * 60 | |||||
#: How many tasks can be called before a sync is forced. | |||||
sync_every_tasks = None | |||||
_last_sync = None | |||||
_tasks_since_sync = 0 | |||||
logger = logger # compat | |||||
def __init__(self, app, schedule=None, max_interval=None, | |||||
Publisher=None, lazy=False, sync_every_tasks=None, **kwargs): | |||||
self.app = app | |||||
self.data = maybe_evaluate({} if schedule is None else schedule) | |||||
self.max_interval = (max_interval or | |||||
app.conf.CELERYBEAT_MAX_LOOP_INTERVAL or | |||||
self.max_interval) | |||||
self.sync_every_tasks = ( | |||||
app.conf.CELERYBEAT_SYNC_EVERY if sync_every_tasks is None | |||||
else sync_every_tasks) | |||||
self.Publisher = Publisher or app.amqp.TaskProducer | |||||
if not lazy: | |||||
self.setup_schedule() | |||||
def install_default_entries(self, data): | |||||
entries = {} | |||||
if self.app.conf.CELERY_TASK_RESULT_EXPIRES and \ | |||||
not self.app.backend.supports_autoexpire: | |||||
if 'celery.backend_cleanup' not in data: | |||||
entries['celery.backend_cleanup'] = { | |||||
'task': 'celery.backend_cleanup', | |||||
'schedule': crontab('0', '4', '*'), | |||||
'options': {'expires': 12 * 3600}} | |||||
self.update_from_dict(entries) | |||||
def maybe_due(self, entry, publisher=None): | |||||
is_due, next_time_to_run = entry.is_due() | |||||
if is_due: | |||||
info('Scheduler: Sending due task %s (%s)', entry.name, entry.task) | |||||
try: | |||||
result = self.apply_async(entry, publisher=publisher) | |||||
except Exception as exc: | |||||
error('Message Error: %s\n%s', | |||||
exc, traceback.format_stack(), exc_info=True) | |||||
else: | |||||
debug('%s sent. id->%s', entry.task, result.id) | |||||
return next_time_to_run | |||||
def tick(self): | |||||
"""Run a tick, that is one iteration of the scheduler. | |||||
Executes all due tasks. | |||||
""" | |||||
remaining_times = [] | |||||
try: | |||||
for entry in values(self.schedule): | |||||
next_time_to_run = self.maybe_due(entry, self.publisher) | |||||
if next_time_to_run: | |||||
remaining_times.append(next_time_to_run) | |||||
except RuntimeError: | |||||
pass | |||||
return min(remaining_times + [self.max_interval]) | |||||
def should_sync(self): | |||||
return ( | |||||
(not self._last_sync or | |||||
(monotonic() - self._last_sync) > self.sync_every) or | |||||
(self.sync_every_tasks and | |||||
self._tasks_since_sync >= self.sync_every_tasks) | |||||
) | |||||
def reserve(self, entry): | |||||
new_entry = self.schedule[entry.name] = next(entry) | |||||
return new_entry | |||||
def apply_async(self, entry, publisher=None, **kwargs): | |||||
# Update timestamps and run counts before we actually execute, | |||||
# so we have that done if an exception is raised (doesn't schedule | |||||
# forever.) | |||||
entry = self.reserve(entry) | |||||
task = self.app.tasks.get(entry.task) | |||||
try: | |||||
if task: | |||||
result = task.apply_async(entry.args, entry.kwargs, | |||||
publisher=publisher, | |||||
**entry.options) | |||||
else: | |||||
result = self.send_task(entry.task, entry.args, entry.kwargs, | |||||
publisher=publisher, | |||||
**entry.options) | |||||
except Exception as exc: | |||||
reraise(SchedulingError, SchedulingError( | |||||
"Couldn't apply scheduled task {0.name}: {exc}".format( | |||||
entry, exc=exc)), sys.exc_info()[2]) | |||||
finally: | |||||
self._tasks_since_sync += 1 | |||||
if self.should_sync(): | |||||
self._do_sync() | |||||
return result | |||||
def send_task(self, *args, **kwargs): | |||||
return self.app.send_task(*args, **kwargs) | |||||
def setup_schedule(self): | |||||
self.install_default_entries(self.data) | |||||
def _do_sync(self): | |||||
try: | |||||
debug('beat: Synchronizing schedule...') | |||||
self.sync() | |||||
finally: | |||||
self._last_sync = monotonic() | |||||
self._tasks_since_sync = 0 | |||||
def sync(self): | |||||
pass | |||||
def close(self): | |||||
self.sync() | |||||
def add(self, **kwargs): | |||||
entry = self.Entry(app=self.app, **kwargs) | |||||
self.schedule[entry.name] = entry | |||||
return entry | |||||
def _maybe_entry(self, name, entry): | |||||
if isinstance(entry, self.Entry): | |||||
entry.app = self.app | |||||
return entry | |||||
return self.Entry(**dict(entry, name=name, app=self.app)) | |||||
def update_from_dict(self, dict_): | |||||
self.schedule.update(dict( | |||||
(name, self._maybe_entry(name, entry)) | |||||
for name, entry in items(dict_))) | |||||
def merge_inplace(self, b): | |||||
schedule = self.schedule | |||||
A, B = set(schedule), set(b) | |||||
# Remove items from disk not in the schedule anymore. | |||||
for key in A ^ B: | |||||
schedule.pop(key, None) | |||||
# Update and add new items in the schedule | |||||
for key in B: | |||||
entry = self.Entry(**dict(b[key], name=key, app=self.app)) | |||||
if schedule.get(key): | |||||
schedule[key].update(entry) | |||||
else: | |||||
schedule[key] = entry | |||||
def _ensure_connected(self): | |||||
# callback called for each retry while the connection | |||||
# can't be established. | |||||
def _error_handler(exc, interval): | |||||
error('beat: Connection error: %s. ' | |||||
'Trying again in %s seconds...', exc, interval) | |||||
return self.connection.ensure_connection( | |||||
_error_handler, self.app.conf.BROKER_CONNECTION_MAX_RETRIES | |||||
) | |||||
def get_schedule(self): | |||||
return self.data | |||||
def set_schedule(self, schedule): | |||||
self.data = schedule | |||||
schedule = property(get_schedule, set_schedule) | |||||
@cached_property | |||||
def connection(self): | |||||
return self.app.connection() | |||||
@cached_property | |||||
def publisher(self): | |||||
return self.Publisher(self._ensure_connected()) | |||||
@property | |||||
def info(self): | |||||
return '' | |||||
class PersistentScheduler(Scheduler): | |||||
persistence = shelve | |||||
known_suffixes = ('', '.db', '.dat', '.bak', '.dir') | |||||
_store = None | |||||
def __init__(self, *args, **kwargs): | |||||
self.schedule_filename = kwargs.get('schedule_filename') | |||||
Scheduler.__init__(self, *args, **kwargs) | |||||
def _remove_db(self): | |||||
for suffix in self.known_suffixes: | |||||
with platforms.ignore_errno(errno.ENOENT): | |||||
os.remove(self.schedule_filename + suffix) | |||||
def _open_schedule(self): | |||||
return self.persistence.open(self.schedule_filename, writeback=True) | |||||
def _destroy_open_corrupted_schedule(self, exc): | |||||
error('Removing corrupted schedule file %r: %r', | |||||
self.schedule_filename, exc, exc_info=True) | |||||
self._remove_db() | |||||
return self._open_schedule() | |||||
def setup_schedule(self): | |||||
try: | |||||
self._store = self._open_schedule() | |||||
# In some cases there may be different errors from a storage | |||||
# backend for corrupted files. Example - DBPageNotFoundError | |||||
# exception from bsddb. In such case the file will be | |||||
# successfully opened but the error will be raised on first key | |||||
# retrieving. | |||||
self._store.keys() | |||||
except Exception as exc: | |||||
self._store = self._destroy_open_corrupted_schedule(exc) | |||||
for _ in (1, 2): | |||||
try: | |||||
self._store['entries'] | |||||
except KeyError: | |||||
# new schedule db | |||||
try: | |||||
self._store['entries'] = {} | |||||
except KeyError as exc: | |||||
self._store = self._destroy_open_corrupted_schedule(exc) | |||||
continue | |||||
else: | |||||
if '__version__' not in self._store: | |||||
warning('DB Reset: Account for new __version__ field') | |||||
self._store.clear() # remove schedule at 2.2.2 upgrade. | |||||
elif 'tz' not in self._store: | |||||
warning('DB Reset: Account for new tz field') | |||||
self._store.clear() # remove schedule at 3.0.8 upgrade | |||||
elif 'utc_enabled' not in self._store: | |||||
warning('DB Reset: Account for new utc_enabled field') | |||||
self._store.clear() # remove schedule at 3.0.9 upgrade | |||||
break | |||||
tz = self.app.conf.CELERY_TIMEZONE | |||||
stored_tz = self._store.get('tz') | |||||
if stored_tz is not None and stored_tz != tz: | |||||
warning('Reset: Timezone changed from %r to %r', stored_tz, tz) | |||||
self._store.clear() # Timezone changed, reset db! | |||||
utc = self.app.conf.CELERY_ENABLE_UTC | |||||
stored_utc = self._store.get('utc_enabled') | |||||
if stored_utc is not None and stored_utc != utc: | |||||
choices = {True: 'enabled', False: 'disabled'} | |||||
warning('Reset: UTC changed from %s to %s', | |||||
choices[stored_utc], choices[utc]) | |||||
self._store.clear() # UTC setting changed, reset db! | |||||
entries = self._store.setdefault('entries', {}) | |||||
self.merge_inplace(self.app.conf.CELERYBEAT_SCHEDULE) | |||||
self.install_default_entries(self.schedule) | |||||
self._store.update(__version__=__version__, tz=tz, utc_enabled=utc) | |||||
self.sync() | |||||
debug('Current schedule:\n' + '\n'.join( | |||||
repr(entry) for entry in values(entries))) | |||||
def get_schedule(self): | |||||
return self._store['entries'] | |||||
def set_schedule(self, schedule): | |||||
self._store['entries'] = schedule | |||||
schedule = property(get_schedule, set_schedule) | |||||
def sync(self): | |||||
if self._store is not None: | |||||
self._store.sync() | |||||
def close(self): | |||||
self.sync() | |||||
self._store.close() | |||||
@property | |||||
def info(self): | |||||
return ' . db -> {self.schedule_filename}'.format(self=self) | |||||
class Service(object): | |||||
scheduler_cls = PersistentScheduler | |||||
def __init__(self, app, max_interval=None, schedule_filename=None, | |||||
scheduler_cls=None): | |||||
self.app = app | |||||
self.max_interval = (max_interval or | |||||
app.conf.CELERYBEAT_MAX_LOOP_INTERVAL) | |||||
self.scheduler_cls = scheduler_cls or self.scheduler_cls | |||||
self.schedule_filename = ( | |||||
schedule_filename or app.conf.CELERYBEAT_SCHEDULE_FILENAME) | |||||
self._is_shutdown = Event() | |||||
self._is_stopped = Event() | |||||
def __reduce__(self): | |||||
return self.__class__, (self.max_interval, self.schedule_filename, | |||||
self.scheduler_cls, self.app) | |||||
def start(self, embedded_process=False, drift=-0.010): | |||||
info('beat: Starting...') | |||||
debug('beat: Ticking with max interval->%s', | |||||
humanize_seconds(self.scheduler.max_interval)) | |||||
signals.beat_init.send(sender=self) | |||||
if embedded_process: | |||||
signals.beat_embedded_init.send(sender=self) | |||||
platforms.set_process_title('celery beat') | |||||
try: | |||||
while not self._is_shutdown.is_set(): | |||||
interval = self.scheduler.tick() | |||||
interval = interval + drift if interval else interval | |||||
if interval and interval > 0: | |||||
debug('beat: Waking up %s.', | |||||
humanize_seconds(interval, prefix='in ')) | |||||
time.sleep(interval) | |||||
if self.scheduler.should_sync(): | |||||
self.scheduler._do_sync() | |||||
except (KeyboardInterrupt, SystemExit): | |||||
self._is_shutdown.set() | |||||
finally: | |||||
self.sync() | |||||
def sync(self): | |||||
self.scheduler.close() | |||||
self._is_stopped.set() | |||||
def stop(self, wait=False): | |||||
info('beat: Shutting down...') | |||||
self._is_shutdown.set() | |||||
wait and self._is_stopped.wait() # block until shutdown done. | |||||
def get_scheduler(self, lazy=False): | |||||
filename = self.schedule_filename | |||||
scheduler = instantiate(self.scheduler_cls, | |||||
app=self.app, | |||||
schedule_filename=filename, | |||||
max_interval=self.max_interval, | |||||
lazy=lazy) | |||||
return scheduler | |||||
@cached_property | |||||
def scheduler(self): | |||||
return self.get_scheduler() | |||||
class _Threaded(Thread): | |||||
"""Embedded task scheduler using threading.""" | |||||
def __init__(self, app, **kwargs): | |||||
super(_Threaded, self).__init__() | |||||
self.app = app | |||||
self.service = Service(app, **kwargs) | |||||
self.daemon = True | |||||
self.name = 'Beat' | |||||
def run(self): | |||||
self.app.set_current() | |||||
self.service.start() | |||||
def stop(self): | |||||
self.service.stop(wait=True) | |||||
try: | |||||
ensure_multiprocessing() | |||||
except NotImplementedError: # pragma: no cover | |||||
_Process = None | |||||
else: | |||||
class _Process(Process): # noqa | |||||
def __init__(self, app, **kwargs): | |||||
super(_Process, self).__init__() | |||||
self.app = app | |||||
self.service = Service(app, **kwargs) | |||||
self.name = 'Beat' | |||||
def run(self): | |||||
reset_signals(full=False) | |||||
platforms.close_open_fds([ | |||||
sys.__stdin__, sys.__stdout__, sys.__stderr__, | |||||
] + list(iter_open_logger_fds())) | |||||
self.app.set_default() | |||||
self.app.set_current() | |||||
self.service.start(embedded_process=True) | |||||
def stop(self): | |||||
self.service.stop() | |||||
self.terminate() | |||||
def EmbeddedService(app, max_interval=None, **kwargs): | |||||
"""Return embedded clock service. | |||||
:keyword thread: Run threaded instead of as a separate process. | |||||
Uses :mod:`multiprocessing` by default, if available. | |||||
""" | |||||
if kwargs.pop('thread', False) or _Process is None: | |||||
# Need short max interval to be able to stop thread | |||||
# in reasonable time. | |||||
return _Threaded(app, max_interval=1, **kwargs) | |||||
return _Process(app, max_interval=max_interval, **kwargs) |
from __future__ import absolute_import | |||||
from .base import Option | |||||
__all__ = ['Option'] |
# -*- coding: utf-8 -*- | |||||
""" | |||||
The :program:`celery amqp` command. | |||||
.. program:: celery amqp | |||||
""" | |||||
from __future__ import absolute_import, print_function, unicode_literals | |||||
import cmd | |||||
import sys | |||||
import shlex | |||||
import pprint | |||||
from functools import partial | |||||
from itertools import count | |||||
from kombu.utils.encoding import safe_str | |||||
from celery.utils.functional import padlist | |||||
from celery.bin.base import Command | |||||
from celery.five import string_t | |||||
from celery.utils import strtobool | |||||
__all__ = ['AMQPAdmin', 'AMQShell', 'Spec', 'amqp'] | |||||
# Map to coerce strings to other types. | |||||
COERCE = {bool: strtobool} | |||||
HELP_HEADER = """ | |||||
Commands | |||||
-------- | |||||
""".rstrip() | |||||
EXAMPLE_TEXT = """ | |||||
Example: | |||||
-> queue.delete myqueue yes no | |||||
""" | |||||
say = partial(print, file=sys.stderr) | |||||
class Spec(object): | |||||
"""AMQP Command specification. | |||||
Used to convert arguments to Python values and display various help | |||||
and tooltips. | |||||
:param args: see :attr:`args`. | |||||
:keyword returns: see :attr:`returns`. | |||||
.. attribute args:: | |||||
List of arguments this command takes. Should | |||||
contain `(argument_name, argument_type)` tuples. | |||||
.. attribute returns: | |||||
Helpful human string representation of what this command returns. | |||||
May be :const:`None`, to signify the return type is unknown. | |||||
""" | |||||
def __init__(self, *args, **kwargs): | |||||
self.args = args | |||||
self.returns = kwargs.get('returns') | |||||
def coerce(self, index, value): | |||||
"""Coerce value for argument at index.""" | |||||
arg_info = self.args[index] | |||||
arg_type = arg_info[1] | |||||
# Might be a custom way to coerce the string value, | |||||
# so look in the coercion map. | |||||
return COERCE.get(arg_type, arg_type)(value) | |||||
def str_args_to_python(self, arglist): | |||||
"""Process list of string arguments to values according to spec. | |||||
e.g: | |||||
>>> spec = Spec([('queue', str), ('if_unused', bool)]) | |||||
>>> spec.str_args_to_python('pobox', 'true') | |||||
('pobox', True) | |||||
""" | |||||
return tuple( | |||||
self.coerce(index, value) for index, value in enumerate(arglist)) | |||||
def format_response(self, response): | |||||
"""Format the return value of this command in a human-friendly way.""" | |||||
if not self.returns: | |||||
return 'ok.' if response is None else response | |||||
if callable(self.returns): | |||||
return self.returns(response) | |||||
return self.returns.format(response) | |||||
def format_arg(self, name, type, default_value=None): | |||||
if default_value is not None: | |||||
return '{0}:{1}'.format(name, default_value) | |||||
return name | |||||
def format_signature(self): | |||||
return ' '.join(self.format_arg(*padlist(list(arg), 3)) | |||||
for arg in self.args) | |||||
def dump_message(message): | |||||
if message is None: | |||||
return 'No messages in queue. basic.publish something.' | |||||
return {'body': message.body, | |||||
'properties': message.properties, | |||||
'delivery_info': message.delivery_info} | |||||
def format_declare_queue(ret): | |||||
return 'ok. queue:{0} messages:{1} consumers:{2}.'.format(*ret) | |||||
class AMQShell(cmd.Cmd): | |||||
"""AMQP API Shell. | |||||
:keyword connect: Function used to connect to the server, must return | |||||
connection object. | |||||
:keyword silent: If :const:`True`, the commands won't have annoying | |||||
output not relevant when running in non-shell mode. | |||||
.. attribute: builtins | |||||
Mapping of built-in command names -> method names | |||||
.. attribute:: amqp | |||||
Mapping of AMQP API commands and their :class:`Spec`. | |||||
""" | |||||
conn = None | |||||
chan = None | |||||
prompt_fmt = '{self.counter}> ' | |||||
identchars = cmd.IDENTCHARS = '.' | |||||
needs_reconnect = False | |||||
counter = 1 | |||||
inc_counter = count(2) | |||||
builtins = {'EOF': 'do_exit', | |||||
'exit': 'do_exit', | |||||
'help': 'do_help'} | |||||
amqp = { | |||||
'exchange.declare': Spec(('exchange', str), | |||||
('type', str), | |||||
('passive', bool, 'no'), | |||||
('durable', bool, 'no'), | |||||
('auto_delete', bool, 'no'), | |||||
('internal', bool, 'no')), | |||||
'exchange.delete': Spec(('exchange', str), | |||||
('if_unused', bool)), | |||||
'queue.bind': Spec(('queue', str), | |||||
('exchange', str), | |||||
('routing_key', str)), | |||||
'queue.declare': Spec(('queue', str), | |||||
('passive', bool, 'no'), | |||||
('durable', bool, 'no'), | |||||
('exclusive', bool, 'no'), | |||||
('auto_delete', bool, 'no'), | |||||
returns=format_declare_queue), | |||||
'queue.delete': Spec(('queue', str), | |||||
('if_unused', bool, 'no'), | |||||
('if_empty', bool, 'no'), | |||||
returns='ok. {0} messages deleted.'), | |||||
'queue.purge': Spec(('queue', str), | |||||
returns='ok. {0} messages deleted.'), | |||||
'basic.get': Spec(('queue', str), | |||||
('no_ack', bool, 'off'), | |||||
returns=dump_message), | |||||
'basic.publish': Spec(('msg', str), | |||||
('exchange', str), | |||||
('routing_key', str), | |||||
('mandatory', bool, 'no'), | |||||
('immediate', bool, 'no')), | |||||
'basic.ack': Spec(('delivery_tag', int)), | |||||
} | |||||
def _prepare_spec(self, conn): | |||||
# XXX Hack to fix Issue #2013 | |||||
from amqp import Connection, Message | |||||
if isinstance(conn.connection, Connection): | |||||
self.amqp['basic.publish'] = Spec(('msg', Message), | |||||
('exchange', str), | |||||
('routing_key', str), | |||||
('mandatory', bool, 'no'), | |||||
('immediate', bool, 'no')) | |||||
def __init__(self, *args, **kwargs): | |||||
self.connect = kwargs.pop('connect') | |||||
self.silent = kwargs.pop('silent', False) | |||||
self.out = kwargs.pop('out', sys.stderr) | |||||
cmd.Cmd.__init__(self, *args, **kwargs) | |||||
self._reconnect() | |||||
def note(self, m): | |||||
"""Say something to the user. Disabled if :attr:`silent`.""" | |||||
if not self.silent: | |||||
say(m, file=self.out) | |||||
def say(self, m): | |||||
say(m, file=self.out) | |||||
def get_amqp_api_command(self, cmd, arglist): | |||||
"""With a command name and a list of arguments, convert the arguments | |||||
to Python values and find the corresponding method on the AMQP channel | |||||
object. | |||||
:returns: tuple of `(method, processed_args)`. | |||||
""" | |||||
spec = self.amqp[cmd] | |||||
args = spec.str_args_to_python(arglist) | |||||
attr_name = cmd.replace('.', '_') | |||||
if self.needs_reconnect: | |||||
self._reconnect() | |||||
return getattr(self.chan, attr_name), args, spec.format_response | |||||
def do_exit(self, *args): | |||||
"""The `'exit'` command.""" | |||||
self.note("\n-> please, don't leave!") | |||||
sys.exit(0) | |||||
def display_command_help(self, cmd, short=False): | |||||
spec = self.amqp[cmd] | |||||
self.say('{0} {1}'.format(cmd, spec.format_signature())) | |||||
def do_help(self, *args): | |||||
if not args: | |||||
self.say(HELP_HEADER) | |||||
for cmd_name in self.amqp: | |||||
self.display_command_help(cmd_name, short=True) | |||||
self.say(EXAMPLE_TEXT) | |||||
else: | |||||
self.display_command_help(args[0]) | |||||
def default(self, line): | |||||
self.say("unknown syntax: {0!r}. how about some 'help'?".format(line)) | |||||
def get_names(self): | |||||
return set(self.builtins) | set(self.amqp) | |||||
def completenames(self, text, *ignored): | |||||
"""Return all commands starting with `text`, for tab-completion.""" | |||||
names = self.get_names() | |||||
first = [cmd for cmd in names | |||||
if cmd.startswith(text.replace('_', '.'))] | |||||
if first: | |||||
return first | |||||
return [cmd for cmd in names | |||||
if cmd.partition('.')[2].startswith(text)] | |||||
def dispatch(self, cmd, argline): | |||||
"""Dispatch and execute the command. | |||||
Lookup order is: :attr:`builtins` -> :attr:`amqp`. | |||||
""" | |||||
arglist = shlex.split(safe_str(argline)) | |||||
if cmd in self.builtins: | |||||
return getattr(self, self.builtins[cmd])(*arglist) | |||||
fun, args, formatter = self.get_amqp_api_command(cmd, arglist) | |||||
return formatter(fun(*args)) | |||||
def parseline(self, line): | |||||
"""Parse input line. | |||||
:returns: tuple of three items: | |||||
`(command_name, arglist, original_line)` | |||||
""" | |||||
parts = line.split() | |||||
if parts: | |||||
return parts[0], ' '.join(parts[1:]), line | |||||
return '', '', line | |||||
def onecmd(self, line): | |||||
"""Parse line and execute command.""" | |||||
cmd, arg, line = self.parseline(line) | |||||
if not line: | |||||
return self.emptyline() | |||||
self.lastcmd = line | |||||
self.counter = next(self.inc_counter) | |||||
try: | |||||
self.respond(self.dispatch(cmd, arg)) | |||||
except (AttributeError, KeyError) as exc: | |||||
self.default(line) | |||||
except Exception as exc: | |||||
self.say(exc) | |||||
self.needs_reconnect = True | |||||
def respond(self, retval): | |||||
"""What to do with the return value of a command.""" | |||||
if retval is not None: | |||||
if isinstance(retval, string_t): | |||||
self.say(retval) | |||||
else: | |||||
self.say(pprint.pformat(retval)) | |||||
def _reconnect(self): | |||||
"""Re-establish connection to the AMQP server.""" | |||||
self.conn = self.connect(self.conn) | |||||
self._prepare_spec(self.conn) | |||||
self.chan = self.conn.default_channel | |||||
self.needs_reconnect = False | |||||
@property | |||||
def prompt(self): | |||||
return self.prompt_fmt.format(self=self) | |||||
class AMQPAdmin(object): | |||||
"""The celery :program:`celery amqp` utility.""" | |||||
Shell = AMQShell | |||||
def __init__(self, *args, **kwargs): | |||||
self.app = kwargs['app'] | |||||
self.out = kwargs.setdefault('out', sys.stderr) | |||||
self.silent = kwargs.get('silent') | |||||
self.args = args | |||||
def connect(self, conn=None): | |||||
if conn: | |||||
conn.close() | |||||
conn = self.app.connection() | |||||
self.note('-> connecting to {0}.'.format(conn.as_uri())) | |||||
conn.connect() | |||||
self.note('-> connected.') | |||||
return conn | |||||
def run(self): | |||||
shell = self.Shell(connect=self.connect, out=self.out) | |||||
if self.args: | |||||
return shell.onecmd(' '.join(self.args)) | |||||
try: | |||||
return shell.cmdloop() | |||||
except KeyboardInterrupt: | |||||
self.note('(bibi)') | |||||
pass | |||||
def note(self, m): | |||||
if not self.silent: | |||||
say(m, file=self.out) | |||||
class amqp(Command): | |||||
"""AMQP Administration Shell. | |||||
Also works for non-amqp transports (but not ones that | |||||
store declarations in memory). | |||||
Examples:: | |||||
celery amqp | |||||
start shell mode | |||||
celery amqp help | |||||
show list of commands | |||||
celery amqp exchange.delete name | |||||
celery amqp queue.delete queue | |||||
celery amqp queue.delete queue yes yes | |||||
""" | |||||
def run(self, *args, **options): | |||||
options['app'] = self.app | |||||
return AMQPAdmin(*args, **options).run() | |||||
def main(): | |||||
amqp().execute_from_commandline() | |||||
if __name__ == '__main__': # pragma: no cover | |||||
main() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
.. _preload-options: | |||||
Preload Options | |||||
--------------- | |||||
These options are supported by all commands, | |||||
and usually parsed before command-specific arguments. | |||||
.. cmdoption:: -A, --app | |||||
app instance to use (e.g. module.attr_name) | |||||
.. cmdoption:: -b, --broker | |||||
url to broker. default is 'amqp://guest@localhost//' | |||||
.. cmdoption:: --loader | |||||
name of custom loader class to use. | |||||
.. cmdoption:: --config | |||||
Name of the configuration module | |||||
.. _daemon-options: | |||||
Daemon Options | |||||
-------------- | |||||
These options are supported by commands that can detach | |||||
into the background (daemon). They will be present | |||||
in any command that also has a `--detach` option. | |||||
.. cmdoption:: -f, --logfile | |||||
Path to log file. If no logfile is specified, `stderr` is used. | |||||
.. cmdoption:: --pidfile | |||||
Optional file used to store the process pid. | |||||
The program will not start if this file already exists | |||||
and the pid is still alive. | |||||
.. cmdoption:: --uid | |||||
User id, or user name of the user to run as after detaching. | |||||
.. cmdoption:: --gid | |||||
Group id, or group name of the main group to change to after | |||||
detaching. | |||||
.. cmdoption:: --umask | |||||
Effective umask (in octal) of the process after detaching. Inherits | |||||
the umask of the parent process by default. | |||||
.. cmdoption:: --workdir | |||||
Optional directory to change to after detaching. | |||||
.. cmdoption:: --executable | |||||
Executable to use for the detached process. | |||||
""" | |||||
from __future__ import absolute_import, print_function, unicode_literals | |||||
import os | |||||
import random | |||||
import re | |||||
import sys | |||||
import warnings | |||||
import json | |||||
from collections import defaultdict | |||||
from heapq import heappush | |||||
from inspect import getargspec | |||||
from optparse import OptionParser, IndentedHelpFormatter, make_option as Option | |||||
from pprint import pformat | |||||
from celery import VERSION_BANNER, Celery, maybe_patch_concurrency | |||||
from celery import signals | |||||
from celery.exceptions import CDeprecationWarning, CPendingDeprecationWarning | |||||
from celery.five import items, string, string_t | |||||
from celery.platforms import EX_FAILURE, EX_OK, EX_USAGE | |||||
from celery.utils import term | |||||
from celery.utils import text | |||||
from celery.utils import node_format, host_format | |||||
from celery.utils.imports import symbol_by_name, import_from_cwd | |||||
try: | |||||
input = raw_input | |||||
except NameError: | |||||
pass | |||||
# always enable DeprecationWarnings, so our users can see them. | |||||
for warning in (CDeprecationWarning, CPendingDeprecationWarning): | |||||
warnings.simplefilter('once', warning, 0) | |||||
ARGV_DISABLED = """ | |||||
Unrecognized command-line arguments: {0} | |||||
Try --help? | |||||
""" | |||||
find_long_opt = re.compile(r'.+?(--.+?)(?:\s|,|$)') | |||||
find_rst_ref = re.compile(r':\w+:`(.+?)`') | |||||
__all__ = ['Error', 'UsageError', 'Extensions', 'HelpFormatter', | |||||
'Command', 'Option', 'daemon_options'] | |||||
class Error(Exception): | |||||
status = EX_FAILURE | |||||
def __init__(self, reason, status=None): | |||||
self.reason = reason | |||||
self.status = status if status is not None else self.status | |||||
super(Error, self).__init__(reason, status) | |||||
def __str__(self): | |||||
return self.reason | |||||
__unicode__ = __str__ | |||||
class UsageError(Error): | |||||
status = EX_USAGE | |||||
class Extensions(object): | |||||
def __init__(self, namespace, register): | |||||
self.names = [] | |||||
self.namespace = namespace | |||||
self.register = register | |||||
def add(self, cls, name): | |||||
heappush(self.names, name) | |||||
self.register(cls, name=name) | |||||
def load(self): | |||||
try: | |||||
from pkg_resources import iter_entry_points | |||||
except ImportError: # pragma: no cover | |||||
return | |||||
for ep in iter_entry_points(self.namespace): | |||||
sym = ':'.join([ep.module_name, ep.attrs[0]]) | |||||
try: | |||||
cls = symbol_by_name(sym) | |||||
except (ImportError, SyntaxError) as exc: | |||||
warnings.warn( | |||||
'Cannot load extension {0!r}: {1!r}'.format(sym, exc)) | |||||
else: | |||||
self.add(cls, ep.name) | |||||
return self.names | |||||
class HelpFormatter(IndentedHelpFormatter): | |||||
def format_epilog(self, epilog): | |||||
if epilog: | |||||
return '\n{0}\n\n'.format(epilog) | |||||
return '' | |||||
def format_description(self, description): | |||||
return text.ensure_2lines(text.fill_paragraphs( | |||||
text.dedent(description), self.width)) | |||||
class Command(object): | |||||
"""Base class for command-line applications. | |||||
:keyword app: The current app. | |||||
:keyword get_app: Callable returning the current app if no app provided. | |||||
""" | |||||
Error = Error | |||||
UsageError = UsageError | |||||
Parser = OptionParser | |||||
#: Arg list used in help. | |||||
args = '' | |||||
#: Application version. | |||||
version = VERSION_BANNER | |||||
#: If false the parser will raise an exception if positional | |||||
#: args are provided. | |||||
supports_args = True | |||||
#: List of options (without preload options). | |||||
option_list = () | |||||
# module Rst documentation to parse help from (if any) | |||||
doc = None | |||||
# Some programs (multi) does not want to load the app specified | |||||
# (Issue #1008). | |||||
respects_app_option = True | |||||
#: List of options to parse before parsing other options. | |||||
preload_options = ( | |||||
Option('-A', '--app', default=None), | |||||
Option('-b', '--broker', default=None), | |||||
Option('--loader', default=None), | |||||
Option('--config', default=None), | |||||
Option('--workdir', default=None, dest='working_directory'), | |||||
Option('--no-color', '-C', action='store_true', default=None), | |||||
Option('--quiet', '-q', action='store_true'), | |||||
) | |||||
#: Enable if the application should support config from the cmdline. | |||||
enable_config_from_cmdline = False | |||||
#: Default configuration namespace. | |||||
namespace = 'celery' | |||||
#: Text to print at end of --help | |||||
epilog = None | |||||
#: Text to print in --help before option list. | |||||
description = '' | |||||
#: Set to true if this command doesn't have subcommands | |||||
leaf = True | |||||
# used by :meth:`say_remote_command_reply`. | |||||
show_body = True | |||||
# used by :meth:`say_chat`. | |||||
show_reply = True | |||||
prog_name = 'celery' | |||||
def __init__(self, app=None, get_app=None, no_color=False, | |||||
stdout=None, stderr=None, quiet=False, on_error=None, | |||||
on_usage_error=None): | |||||
self.app = app | |||||
self.get_app = get_app or self._get_default_app | |||||
self.stdout = stdout or sys.stdout | |||||
self.stderr = stderr or sys.stderr | |||||
self._colored = None | |||||
self._no_color = no_color | |||||
self.quiet = quiet | |||||
if not self.description: | |||||
self.description = self.__doc__ | |||||
if on_error: | |||||
self.on_error = on_error | |||||
if on_usage_error: | |||||
self.on_usage_error = on_usage_error | |||||
def run(self, *args, **options): | |||||
"""This is the body of the command called by :meth:`handle_argv`.""" | |||||
raise NotImplementedError('subclass responsibility') | |||||
def on_error(self, exc): | |||||
self.error(self.colored.red('Error: {0}'.format(exc))) | |||||
def on_usage_error(self, exc): | |||||
self.handle_error(exc) | |||||
def on_concurrency_setup(self): | |||||
pass | |||||
def __call__(self, *args, **kwargs): | |||||
random.seed() # maybe we were forked. | |||||
self.verify_args(args) | |||||
try: | |||||
ret = self.run(*args, **kwargs) | |||||
return ret if ret is not None else EX_OK | |||||
except self.UsageError as exc: | |||||
self.on_usage_error(exc) | |||||
return exc.status | |||||
except self.Error as exc: | |||||
self.on_error(exc) | |||||
return exc.status | |||||
def verify_args(self, given, _index=0): | |||||
S = getargspec(self.run) | |||||
_index = 1 if S.args and S.args[0] == 'self' else _index | |||||
required = S.args[_index:-len(S.defaults) if S.defaults else None] | |||||
missing = required[len(given):] | |||||
if missing: | |||||
raise self.UsageError('Missing required {0}: {1}'.format( | |||||
text.pluralize(len(missing), 'argument'), | |||||
', '.join(missing) | |||||
)) | |||||
def execute_from_commandline(self, argv=None): | |||||
"""Execute application from command-line. | |||||
:keyword argv: The list of command-line arguments. | |||||
Defaults to ``sys.argv``. | |||||
""" | |||||
if argv is None: | |||||
argv = list(sys.argv) | |||||
# Should we load any special concurrency environment? | |||||
self.maybe_patch_concurrency(argv) | |||||
self.on_concurrency_setup() | |||||
# Dump version and exit if '--version' arg set. | |||||
self.early_version(argv) | |||||
argv = self.setup_app_from_commandline(argv) | |||||
self.prog_name = os.path.basename(argv[0]) | |||||
return self.handle_argv(self.prog_name, argv[1:]) | |||||
def run_from_argv(self, prog_name, argv=None, command=None): | |||||
return self.handle_argv(prog_name, | |||||
sys.argv if argv is None else argv, command) | |||||
def maybe_patch_concurrency(self, argv=None): | |||||
argv = argv or sys.argv | |||||
pool_option = self.with_pool_option(argv) | |||||
if pool_option: | |||||
maybe_patch_concurrency(argv, *pool_option) | |||||
short_opts, long_opts = pool_option | |||||
def usage(self, command): | |||||
return '%prog {0} [options] {self.args}'.format(command, self=self) | |||||
def get_options(self): | |||||
"""Get supported command-line options.""" | |||||
return self.option_list | |||||
def expanduser(self, value): | |||||
if isinstance(value, string_t): | |||||
return os.path.expanduser(value) | |||||
return value | |||||
def ask(self, q, choices, default=None): | |||||
"""Prompt user to choose from a tuple of string values. | |||||
:param q: the question to ask (do not include questionark) | |||||
:param choice: tuple of possible choices, must be lowercase. | |||||
:param default: Default value if any. | |||||
If a default is not specified the question will be repeated | |||||
until the user gives a valid choice. | |||||
Matching is done case insensitively. | |||||
""" | |||||
schoices = choices | |||||
if default is not None: | |||||
schoices = [c.upper() if c == default else c.lower() | |||||
for c in choices] | |||||
schoices = '/'.join(schoices) | |||||
p = '{0} ({1})? '.format(q.capitalize(), schoices) | |||||
while 1: | |||||
val = input(p).lower() | |||||
if val in choices: | |||||
return val | |||||
elif default is not None: | |||||
break | |||||
return default | |||||
def handle_argv(self, prog_name, argv, command=None): | |||||
"""Parse command-line arguments from ``argv`` and dispatch | |||||
to :meth:`run`. | |||||
:param prog_name: The program name (``argv[0]``). | |||||
:param argv: Command arguments. | |||||
Exits with an error message if :attr:`supports_args` is disabled | |||||
and ``argv`` contains positional arguments. | |||||
""" | |||||
options, args = self.prepare_args( | |||||
*self.parse_options(prog_name, argv, command)) | |||||
return self(*args, **options) | |||||
def prepare_args(self, options, args): | |||||
if options: | |||||
options = dict((k, self.expanduser(v)) | |||||
for k, v in items(vars(options)) | |||||
if not k.startswith('_')) | |||||
args = [self.expanduser(arg) for arg in args] | |||||
self.check_args(args) | |||||
return options, args | |||||
def check_args(self, args): | |||||
if not self.supports_args and args: | |||||
self.die(ARGV_DISABLED.format(', '.join(args)), EX_USAGE) | |||||
def error(self, s): | |||||
self.out(s, fh=self.stderr) | |||||
def out(self, s, fh=None): | |||||
print(s, file=fh or self.stdout) | |||||
def die(self, msg, status=EX_FAILURE): | |||||
self.error(msg) | |||||
sys.exit(status) | |||||
def early_version(self, argv): | |||||
if '--version' in argv: | |||||
print(self.version, file=self.stdout) | |||||
sys.exit(0) | |||||
def parse_options(self, prog_name, arguments, command=None): | |||||
"""Parse the available options.""" | |||||
# Don't want to load configuration to just print the version, | |||||
# so we handle --version manually here. | |||||
self.parser = self.create_parser(prog_name, command) | |||||
return self.parser.parse_args(arguments) | |||||
def create_parser(self, prog_name, command=None): | |||||
option_list = ( | |||||
self.preload_options + | |||||
self.get_options() + | |||||
tuple(self.app.user_options['preload']) | |||||
) | |||||
return self.prepare_parser(self.Parser( | |||||
prog=prog_name, | |||||
usage=self.usage(command), | |||||
version=self.version, | |||||
epilog=self.epilog, | |||||
formatter=HelpFormatter(), | |||||
description=self.description, | |||||
option_list=option_list, | |||||
)) | |||||
def prepare_parser(self, parser): | |||||
docs = [self.parse_doc(doc) for doc in (self.doc, __doc__) if doc] | |||||
for doc in docs: | |||||
for long_opt, help in items(doc): | |||||
option = parser.get_option(long_opt) | |||||
if option is not None: | |||||
option.help = ' '.join(help).format(default=option.default) | |||||
return parser | |||||
def setup_app_from_commandline(self, argv): | |||||
preload_options = self.parse_preload_options(argv) | |||||
quiet = preload_options.get('quiet') | |||||
if quiet is not None: | |||||
self.quiet = quiet | |||||
try: | |||||
self.no_color = preload_options['no_color'] | |||||
except KeyError: | |||||
pass | |||||
workdir = preload_options.get('working_directory') | |||||
if workdir: | |||||
os.chdir(workdir) | |||||
app = (preload_options.get('app') or | |||||
os.environ.get('CELERY_APP') or | |||||
self.app) | |||||
preload_loader = preload_options.get('loader') | |||||
if preload_loader: | |||||
# Default app takes loader from this env (Issue #1066). | |||||
os.environ['CELERY_LOADER'] = preload_loader | |||||
loader = (preload_loader, | |||||
os.environ.get('CELERY_LOADER') or | |||||
'default') | |||||
broker = preload_options.get('broker', None) | |||||
if broker: | |||||
os.environ['CELERY_BROKER_URL'] = broker | |||||
config = preload_options.get('config') | |||||
if config: | |||||
os.environ['CELERY_CONFIG_MODULE'] = config | |||||
if self.respects_app_option: | |||||
if app: | |||||
self.app = self.find_app(app) | |||||
elif self.app is None: | |||||
self.app = self.get_app(loader=loader) | |||||
if self.enable_config_from_cmdline: | |||||
argv = self.process_cmdline_config(argv) | |||||
else: | |||||
self.app = Celery(fixups=[]) | |||||
user_preload = tuple(self.app.user_options['preload'] or ()) | |||||
if user_preload: | |||||
user_options = self.preparse_options(argv, user_preload) | |||||
for user_option in user_preload: | |||||
user_options.setdefault(user_option.dest, user_option.default) | |||||
signals.user_preload_options.send( | |||||
sender=self, app=self.app, options=user_options, | |||||
) | |||||
return argv | |||||
def find_app(self, app): | |||||
from celery.app.utils import find_app | |||||
return find_app(app, symbol_by_name=self.symbol_by_name) | |||||
def symbol_by_name(self, name, imp=import_from_cwd): | |||||
return symbol_by_name(name, imp=imp) | |||||
get_cls_by_name = symbol_by_name # XXX compat | |||||
def process_cmdline_config(self, argv): | |||||
try: | |||||
cargs_start = argv.index('--') | |||||
except ValueError: | |||||
return argv | |||||
argv, cargs = argv[:cargs_start], argv[cargs_start + 1:] | |||||
self.app.config_from_cmdline(cargs, namespace=self.namespace) | |||||
return argv | |||||
def parse_preload_options(self, args): | |||||
return self.preparse_options(args, self.preload_options) | |||||
def add_append_opt(self, acc, opt, value): | |||||
acc.setdefault(opt.dest, opt.default or []) | |||||
acc[opt.dest].append(value) | |||||
def preparse_options(self, args, options): | |||||
acc = {} | |||||
opts = {} | |||||
for opt in options: | |||||
for t in (opt._long_opts, opt._short_opts): | |||||
opts.update(dict(zip(t, [opt] * len(t)))) | |||||
index = 0 | |||||
length = len(args) | |||||
while index < length: | |||||
arg = args[index] | |||||
if arg.startswith('--'): | |||||
if '=' in arg: | |||||
key, value = arg.split('=', 1) | |||||
opt = opts.get(key) | |||||
if opt: | |||||
if opt.action == 'append': | |||||
self.add_append_opt(acc, opt, value) | |||||
else: | |||||
acc[opt.dest] = value | |||||
else: | |||||
opt = opts.get(arg) | |||||
if opt and opt.takes_value(): | |||||
# optparse also supports ['--opt', 'value'] | |||||
# (Issue #1668) | |||||
if opt.action == 'append': | |||||
self.add_append_opt(acc, opt, args[index + 1]) | |||||
else: | |||||
acc[opt.dest] = args[index + 1] | |||||
index += 1 | |||||
elif opt and opt.action == 'store_true': | |||||
acc[opt.dest] = True | |||||
elif arg.startswith('-'): | |||||
opt = opts.get(arg) | |||||
if opt: | |||||
if opt.takes_value(): | |||||
try: | |||||
acc[opt.dest] = args[index + 1] | |||||
except IndexError: | |||||
raise ValueError( | |||||
'Missing required argument for {0}'.format( | |||||
arg)) | |||||
index += 1 | |||||
elif opt.action == 'store_true': | |||||
acc[opt.dest] = True | |||||
index += 1 | |||||
return acc | |||||
def parse_doc(self, doc): | |||||
options, in_option = defaultdict(list), None | |||||
for line in doc.splitlines(): | |||||
if line.startswith('.. cmdoption::'): | |||||
m = find_long_opt.match(line) | |||||
if m: | |||||
in_option = m.groups()[0].strip() | |||||
assert in_option, 'missing long opt' | |||||
elif in_option and line.startswith(' ' * 4): | |||||
options[in_option].append( | |||||
find_rst_ref.sub(r'\1', line.strip()).replace('`', '')) | |||||
return options | |||||
def with_pool_option(self, argv): | |||||
"""Return tuple of ``(short_opts, long_opts)`` if the command | |||||
supports a pool argument, and used to monkey patch eventlet/gevent | |||||
environments as early as possible. | |||||
E.g:: | |||||
has_pool_option = (['-P'], ['--pool']) | |||||
""" | |||||
pass | |||||
def node_format(self, s, nodename, **extra): | |||||
return node_format(s, nodename, **extra) | |||||
def host_format(self, s, **extra): | |||||
return host_format(s, **extra) | |||||
def _get_default_app(self, *args, **kwargs): | |||||
from celery._state import get_current_app | |||||
return get_current_app() # omit proxy | |||||
def pretty_list(self, n): | |||||
c = self.colored | |||||
if not n: | |||||
return '- empty -' | |||||
return '\n'.join( | |||||
str(c.reset(c.white('*'), ' {0}'.format(item))) for item in n | |||||
) | |||||
def pretty_dict_ok_error(self, n): | |||||
c = self.colored | |||||
try: | |||||
return (c.green('OK'), | |||||
text.indent(self.pretty(n['ok'])[1], 4)) | |||||
except KeyError: | |||||
pass | |||||
return (c.red('ERROR'), | |||||
text.indent(self.pretty(n['error'])[1], 4)) | |||||
def say_remote_command_reply(self, replies): | |||||
c = self.colored | |||||
node = next(iter(replies)) # <-- take first. | |||||
reply = replies[node] | |||||
status, preply = self.pretty(reply) | |||||
self.say_chat('->', c.cyan(node, ': ') + status, | |||||
text.indent(preply, 4) if self.show_reply else '') | |||||
def pretty(self, n): | |||||
OK = str(self.colored.green('OK')) | |||||
if isinstance(n, list): | |||||
return OK, self.pretty_list(n) | |||||
if isinstance(n, dict): | |||||
if 'ok' in n or 'error' in n: | |||||
return self.pretty_dict_ok_error(n) | |||||
else: | |||||
return OK, json.dumps(n, sort_keys=True, indent=4) | |||||
if isinstance(n, string_t): | |||||
return OK, string(n) | |||||
return OK, pformat(n) | |||||
def say_chat(self, direction, title, body=''): | |||||
c = self.colored | |||||
if direction == '<-' and self.quiet: | |||||
return | |||||
dirstr = not self.quiet and c.bold(c.white(direction), ' ') or '' | |||||
self.out(c.reset(dirstr, title)) | |||||
if body and self.show_body: | |||||
self.out(body) | |||||
@property | |||||
def colored(self): | |||||
if self._colored is None: | |||||
self._colored = term.colored(enabled=not self.no_color) | |||||
return self._colored | |||||
@colored.setter | |||||
def colored(self, obj): | |||||
self._colored = obj | |||||
@property | |||||
def no_color(self): | |||||
return self._no_color | |||||
@no_color.setter | |||||
def no_color(self, value): | |||||
self._no_color = value | |||||
if self._colored is not None: | |||||
self._colored.enabled = not self._no_color | |||||
def daemon_options(default_pidfile=None, default_logfile=None): | |||||
return ( | |||||
Option('-f', '--logfile', default=default_logfile), | |||||
Option('--pidfile', default=default_pidfile), | |||||
Option('--uid', default=None), | |||||
Option('--gid', default=None), | |||||
Option('--umask', default=None), | |||||
Option('--executable', default=None), | |||||
) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
The :program:`celery beat` command. | |||||
.. program:: celery beat | |||||
.. seealso:: | |||||
See :ref:`preload-options` and :ref:`daemon-options`. | |||||
.. cmdoption:: --detach | |||||
Detach and run in the background as a daemon. | |||||
.. cmdoption:: -s, --schedule | |||||
Path to the schedule database. Defaults to `celerybeat-schedule`. | |||||
The extension '.db' may be appended to the filename. | |||||
Default is {default}. | |||||
.. cmdoption:: -S, --scheduler | |||||
Scheduler class to use. | |||||
Default is :class:`celery.beat.PersistentScheduler`. | |||||
.. cmdoption:: --max-interval | |||||
Max seconds to sleep between schedule iterations. | |||||
.. cmdoption:: -f, --logfile | |||||
Path to log file. If no logfile is specified, `stderr` is used. | |||||
.. cmdoption:: -l, --loglevel | |||||
Logging level, choose between `DEBUG`, `INFO`, `WARNING`, | |||||
`ERROR`, `CRITICAL`, or `FATAL`. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from functools import partial | |||||
from celery.platforms import detached, maybe_drop_privileges | |||||
from celery.bin.base import Command, Option, daemon_options | |||||
__all__ = ['beat'] | |||||
class beat(Command): | |||||
"""Start the beat periodic task scheduler. | |||||
Examples:: | |||||
celery beat -l info | |||||
celery beat -s /var/run/celery/beat-schedule --detach | |||||
celery beat -S djcelery.schedulers.DatabaseScheduler | |||||
""" | |||||
doc = __doc__ | |||||
enable_config_from_cmdline = True | |||||
supports_args = False | |||||
def run(self, detach=False, logfile=None, pidfile=None, uid=None, | |||||
gid=None, umask=None, working_directory=None, **kwargs): | |||||
if not detach: | |||||
maybe_drop_privileges(uid=uid, gid=gid) | |||||
workdir = working_directory | |||||
kwargs.pop('app', None) | |||||
beat = partial(self.app.Beat, | |||||
logfile=logfile, pidfile=pidfile, **kwargs) | |||||
if detach: | |||||
with detached(logfile, pidfile, uid, gid, umask, workdir): | |||||
return beat().run() | |||||
else: | |||||
return beat().run() | |||||
def get_options(self): | |||||
c = self.app.conf | |||||
return ( | |||||
(Option('--detach', action='store_true'), | |||||
Option('-s', '--schedule', | |||||
default=c.CELERYBEAT_SCHEDULE_FILENAME), | |||||
Option('--max-interval', type='float'), | |||||
Option('-S', '--scheduler', dest='scheduler_cls'), | |||||
Option('-l', '--loglevel', default=c.CELERYBEAT_LOG_LEVEL)) + | |||||
daemon_options(default_pidfile='celerybeat.pid') + | |||||
tuple(self.app.user_options['beat']) | |||||
) | |||||
def main(app=None): | |||||
beat(app=app).execute_from_commandline() | |||||
if __name__ == '__main__': # pragma: no cover | |||||
main() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
The :program:`celery` umbrella command. | |||||
.. program:: celery | |||||
""" | |||||
from __future__ import absolute_import, unicode_literals | |||||
import anyjson | |||||
import numbers | |||||
import os | |||||
import sys | |||||
from functools import partial | |||||
from importlib import import_module | |||||
from celery.five import string_t, values | |||||
from celery.platforms import EX_OK, EX_FAILURE, EX_UNAVAILABLE, EX_USAGE | |||||
from celery.utils import term | |||||
from celery.utils import text | |||||
from celery.utils.timeutils import maybe_iso8601 | |||||
# Cannot use relative imports here due to a Windows issue (#1111). | |||||
from celery.bin.base import Command, Option, Extensions | |||||
# Import commands from other modules | |||||
from celery.bin.amqp import amqp | |||||
from celery.bin.beat import beat | |||||
from celery.bin.events import events | |||||
from celery.bin.graph import graph | |||||
from celery.bin.worker import worker | |||||
__all__ = ['CeleryCommand', 'main'] | |||||
HELP = """ | |||||
---- -- - - ---- Commands- -------------- --- ------------ | |||||
{commands} | |||||
---- -- - - --------- -- - -------------- --- ------------ | |||||
Type '{prog_name} <command> --help' for help using a specific command. | |||||
""" | |||||
MIGRATE_PROGRESS_FMT = """\ | |||||
Migrating task {state.count}/{state.strtotal}: \ | |||||
{body[task]}[{body[id]}]\ | |||||
""" | |||||
DEBUG = os.environ.get('C_DEBUG', False) | |||||
command_classes = [ | |||||
('Main', ['worker', 'events', 'beat', 'shell', 'multi', 'amqp'], 'green'), | |||||
('Remote Control', ['status', 'inspect', 'control'], 'blue'), | |||||
('Utils', ['purge', 'list', 'migrate', 'call', 'result', 'report'], None), | |||||
] | |||||
if DEBUG: # pragma: no cover | |||||
command_classes.append( | |||||
('Debug', ['graph'], 'red'), | |||||
) | |||||
def determine_exit_status(ret): | |||||
if isinstance(ret, numbers.Integral): | |||||
return ret | |||||
return EX_OK if ret else EX_FAILURE | |||||
def main(argv=None): | |||||
# Fix for setuptools generated scripts, so that it will | |||||
# work with multiprocessing fork emulation. | |||||
# (see multiprocessing.forking.get_preparation_data()) | |||||
try: | |||||
if __name__ != '__main__': # pragma: no cover | |||||
sys.modules['__main__'] = sys.modules[__name__] | |||||
cmd = CeleryCommand() | |||||
cmd.maybe_patch_concurrency() | |||||
from billiard import freeze_support | |||||
freeze_support() | |||||
cmd.execute_from_commandline(argv) | |||||
except KeyboardInterrupt: | |||||
pass | |||||
class multi(Command): | |||||
"""Start multiple worker instances.""" | |||||
respects_app_option = False | |||||
def get_options(self): | |||||
return () | |||||
def run_from_argv(self, prog_name, argv, command=None): | |||||
from celery.bin.multi import MultiTool | |||||
multi = MultiTool(quiet=self.quiet, no_color=self.no_color) | |||||
return multi.execute_from_commandline( | |||||
[command] + argv, prog_name, | |||||
) | |||||
class list_(Command): | |||||
"""Get info from broker. | |||||
Examples:: | |||||
celery list bindings | |||||
NOTE: For RabbitMQ the management plugin is required. | |||||
""" | |||||
args = '[bindings]' | |||||
def list_bindings(self, management): | |||||
try: | |||||
bindings = management.get_bindings() | |||||
except NotImplementedError: | |||||
raise self.Error('Your transport cannot list bindings.') | |||||
def fmt(q, e, r): | |||||
return self.out('{0:<28} {1:<28} {2}'.format(q, e, r)) | |||||
fmt('Queue', 'Exchange', 'Routing Key') | |||||
fmt('-' * 16, '-' * 16, '-' * 16) | |||||
for b in bindings: | |||||
fmt(b['destination'], b['source'], b['routing_key']) | |||||
def run(self, what=None, *_, **kw): | |||||
topics = {'bindings': self.list_bindings} | |||||
available = ', '.join(topics) | |||||
if not what: | |||||
raise self.UsageError( | |||||
'You must specify one of {0}'.format(available)) | |||||
if what not in topics: | |||||
raise self.UsageError( | |||||
'unknown topic {0!r} (choose one of: {1})'.format( | |||||
what, available)) | |||||
with self.app.connection() as conn: | |||||
self.app.amqp.TaskConsumer(conn).declare() | |||||
topics[what](conn.manager) | |||||
class call(Command): | |||||
"""Call a task by name. | |||||
Examples:: | |||||
celery call tasks.add --args='[2, 2]' | |||||
celery call tasks.add --args='[2, 2]' --countdown=10 | |||||
""" | |||||
args = '<task_name>' | |||||
option_list = Command.option_list + ( | |||||
Option('--args', '-a', help='positional arguments (json).'), | |||||
Option('--kwargs', '-k', help='keyword arguments (json).'), | |||||
Option('--eta', help='scheduled time (ISO-8601).'), | |||||
Option('--countdown', type='float', | |||||
help='eta in seconds from now (float/int).'), | |||||
Option('--expires', help='expiry time (ISO-8601/float/int).'), | |||||
Option('--serializer', default='json', help='defaults to json.'), | |||||
Option('--queue', help='custom queue name.'), | |||||
Option('--exchange', help='custom exchange name.'), | |||||
Option('--routing-key', help='custom routing key.'), | |||||
) | |||||
def run(self, name, *_, **kw): | |||||
# Positional args. | |||||
args = kw.get('args') or () | |||||
if isinstance(args, string_t): | |||||
args = anyjson.loads(args) | |||||
# Keyword args. | |||||
kwargs = kw.get('kwargs') or {} | |||||
if isinstance(kwargs, string_t): | |||||
kwargs = anyjson.loads(kwargs) | |||||
# Expires can be int/float. | |||||
expires = kw.get('expires') or None | |||||
try: | |||||
expires = float(expires) | |||||
except (TypeError, ValueError): | |||||
# or a string describing an ISO 8601 datetime. | |||||
try: | |||||
expires = maybe_iso8601(expires) | |||||
except (TypeError, ValueError): | |||||
raise | |||||
res = self.app.send_task(name, args=args, kwargs=kwargs, | |||||
countdown=kw.get('countdown'), | |||||
serializer=kw.get('serializer'), | |||||
queue=kw.get('queue'), | |||||
exchange=kw.get('exchange'), | |||||
routing_key=kw.get('routing_key'), | |||||
eta=maybe_iso8601(kw.get('eta')), | |||||
expires=expires) | |||||
self.out(res.id) | |||||
class purge(Command): | |||||
"""Erase all messages from all known task queues. | |||||
WARNING: There is no undo operation for this command. | |||||
""" | |||||
warn_prelude = ( | |||||
'{warning}: This will remove all tasks from {queues}: {names}.\n' | |||||
' There is no undo for this operation!\n\n' | |||||
'(to skip this prompt use the -f option)\n' | |||||
) | |||||
warn_prompt = 'Are you sure you want to delete all tasks' | |||||
fmt_purged = 'Purged {mnum} {messages} from {qnum} known task {queues}.' | |||||
fmt_empty = 'No messages purged from {qnum} {queues}' | |||||
option_list = Command.option_list + ( | |||||
Option('--force', '-f', action='store_true', | |||||
help='Do not prompt for verification'), | |||||
) | |||||
def run(self, force=False, **kwargs): | |||||
names = list(sorted(self.app.amqp.queues.keys())) | |||||
qnum = len(names) | |||||
if not force: | |||||
self.out(self.warn_prelude.format( | |||||
warning=self.colored.red('WARNING'), | |||||
queues=text.pluralize(qnum, 'queue'), names=', '.join(names), | |||||
)) | |||||
if self.ask(self.warn_prompt, ('yes', 'no'), 'no') != 'yes': | |||||
return | |||||
messages = self.app.control.purge() | |||||
fmt = self.fmt_purged if messages else self.fmt_empty | |||||
self.out(fmt.format( | |||||
mnum=messages, qnum=qnum, | |||||
messages=text.pluralize(messages, 'message'), | |||||
queues=text.pluralize(qnum, 'queue'))) | |||||
class result(Command): | |||||
"""Gives the return value for a given task id. | |||||
Examples:: | |||||
celery result 8f511516-e2f5-4da4-9d2f-0fb83a86e500 | |||||
celery result 8f511516-e2f5-4da4-9d2f-0fb83a86e500 -t tasks.add | |||||
celery result 8f511516-e2f5-4da4-9d2f-0fb83a86e500 --traceback | |||||
""" | |||||
args = '<task_id>' | |||||
option_list = Command.option_list + ( | |||||
Option('--task', '-t', help='name of task (if custom backend)'), | |||||
Option('--traceback', action='store_true', | |||||
help='show traceback instead'), | |||||
) | |||||
def run(self, task_id, *args, **kwargs): | |||||
result_cls = self.app.AsyncResult | |||||
task = kwargs.get('task') | |||||
traceback = kwargs.get('traceback', False) | |||||
if task: | |||||
result_cls = self.app.tasks[task].AsyncResult | |||||
result = result_cls(task_id) | |||||
if traceback: | |||||
value = result.traceback | |||||
else: | |||||
value = result.get() | |||||
self.out(self.pretty(value)[1]) | |||||
class _RemoteControl(Command): | |||||
name = None | |||||
choices = None | |||||
leaf = False | |||||
option_list = Command.option_list + ( | |||||
Option('--timeout', '-t', type='float', | |||||
help='Timeout in seconds (float) waiting for reply'), | |||||
Option('--destination', '-d', | |||||
help='Comma separated list of destination node names.')) | |||||
def __init__(self, *args, **kwargs): | |||||
self.show_body = kwargs.pop('show_body', True) | |||||
self.show_reply = kwargs.pop('show_reply', True) | |||||
super(_RemoteControl, self).__init__(*args, **kwargs) | |||||
@classmethod | |||||
def get_command_info(self, command, | |||||
indent=0, prefix='', color=None, help=False): | |||||
if help: | |||||
help = '|' + text.indent(self.choices[command][1], indent + 4) | |||||
else: | |||||
help = None | |||||
try: | |||||
# see if it uses args. | |||||
meth = getattr(self, command) | |||||
return text.join([ | |||||
'|' + text.indent('{0}{1} {2}'.format( | |||||
prefix, color(command), meth.__doc__), indent), | |||||
help, | |||||
]) | |||||
except AttributeError: | |||||
return text.join([ | |||||
'|' + text.indent(prefix + str(color(command)), indent), help, | |||||
]) | |||||
@classmethod | |||||
def list_commands(self, indent=0, prefix='', color=None, help=False): | |||||
color = color if color else lambda x: x | |||||
prefix = prefix + ' ' if prefix else '' | |||||
return '\n'.join(self.get_command_info(c, indent, prefix, color, help) | |||||
for c in sorted(self.choices)) | |||||
@property | |||||
def epilog(self): | |||||
return '\n'.join([ | |||||
'[Commands]', | |||||
self.list_commands(indent=4, help=True) | |||||
]) | |||||
def usage(self, command): | |||||
return '%prog {0} [options] {1} <command> [arg1 .. argN]'.format( | |||||
command, self.args) | |||||
def call(self, *args, **kwargs): | |||||
raise NotImplementedError('call') | |||||
def run(self, *args, **kwargs): | |||||
if not args: | |||||
raise self.UsageError( | |||||
'Missing {0.name} method. See --help'.format(self)) | |||||
return self.do_call_method(args, **kwargs) | |||||
def do_call_method(self, args, **kwargs): | |||||
method = args[0] | |||||
if method == 'help': | |||||
raise self.Error("Did you mean '{0.name} --help'?".format(self)) | |||||
if method not in self.choices: | |||||
raise self.UsageError( | |||||
'Unknown {0.name} method {1}'.format(self, method)) | |||||
if self.app.connection().transport.driver_type == 'sql': | |||||
raise self.Error('Broadcast not supported by SQL broker transport') | |||||
destination = kwargs.get('destination') | |||||
timeout = kwargs.get('timeout') or self.choices[method][0] | |||||
if destination and isinstance(destination, string_t): | |||||
destination = [dest.strip() for dest in destination.split(',')] | |||||
handler = getattr(self, method, self.call) | |||||
replies = handler(method, *args[1:], timeout=timeout, | |||||
destination=destination, | |||||
callback=self.say_remote_command_reply) | |||||
if not replies: | |||||
raise self.Error('No nodes replied within time constraint.', | |||||
status=EX_UNAVAILABLE) | |||||
return replies | |||||
class inspect(_RemoteControl): | |||||
"""Inspect the worker at runtime. | |||||
Availability: RabbitMQ (amqp), Redis, and MongoDB transports. | |||||
Examples:: | |||||
celery inspect active --timeout=5 | |||||
celery inspect scheduled -d worker1@example.com | |||||
celery inspect revoked -d w1@e.com,w2@e.com | |||||
""" | |||||
name = 'inspect' | |||||
choices = { | |||||
'active': (1.0, 'dump active tasks (being processed)'), | |||||
'active_queues': (1.0, 'dump queues being consumed from'), | |||||
'scheduled': (1.0, 'dump scheduled tasks (eta/countdown/retry)'), | |||||
'reserved': (1.0, 'dump reserved tasks (waiting to be processed)'), | |||||
'stats': (1.0, 'dump worker statistics'), | |||||
'revoked': (1.0, 'dump of revoked task ids'), | |||||
'registered': (1.0, 'dump of registered tasks'), | |||||
'ping': (0.2, 'ping worker(s)'), | |||||
'clock': (1.0, 'get value of logical clock'), | |||||
'conf': (1.0, 'dump worker configuration'), | |||||
'report': (1.0, 'get bugreport info'), | |||||
'memsample': (1.0, 'sample memory (requires psutil)'), | |||||
'memdump': (1.0, 'dump memory samples (requires psutil)'), | |||||
'objgraph': (60.0, 'create object graph (requires objgraph)'), | |||||
} | |||||
def call(self, method, *args, **options): | |||||
i = self.app.control.inspect(**options) | |||||
return getattr(i, method)(*args) | |||||
def objgraph(self, type_='Request', *args, **kwargs): | |||||
return self.call('objgraph', type_, **kwargs) | |||||
def conf(self, with_defaults=False, *args, **kwargs): | |||||
return self.call('conf', with_defaults, **kwargs) | |||||
class control(_RemoteControl): | |||||
"""Workers remote control. | |||||
Availability: RabbitMQ (amqp), Redis, and MongoDB transports. | |||||
Examples:: | |||||
celery control enable_events --timeout=5 | |||||
celery control -d worker1@example.com enable_events | |||||
celery control -d w1.e.com,w2.e.com enable_events | |||||
celery control -d w1.e.com add_consumer queue_name | |||||
celery control -d w1.e.com cancel_consumer queue_name | |||||
celery control -d w1.e.com add_consumer queue exchange direct rkey | |||||
""" | |||||
name = 'control' | |||||
choices = { | |||||
'enable_events': (1.0, 'tell worker(s) to enable events'), | |||||
'disable_events': (1.0, 'tell worker(s) to disable events'), | |||||
'add_consumer': (1.0, 'tell worker(s) to start consuming a queue'), | |||||
'cancel_consumer': (1.0, 'tell worker(s) to stop consuming a queue'), | |||||
'rate_limit': ( | |||||
1.0, 'tell worker(s) to modify the rate limit for a task type'), | |||||
'time_limit': ( | |||||
1.0, 'tell worker(s) to modify the time limit for a task type.'), | |||||
'autoscale': (1.0, 'change autoscale settings'), | |||||
'pool_grow': (1.0, 'start more pool processes'), | |||||
'pool_shrink': (1.0, 'use less pool processes'), | |||||
} | |||||
def call(self, method, *args, **options): | |||||
return getattr(self.app.control, method)(*args, reply=True, **options) | |||||
def pool_grow(self, method, n=1, **kwargs): | |||||
"""[N=1]""" | |||||
return self.call(method, int(n), **kwargs) | |||||
def pool_shrink(self, method, n=1, **kwargs): | |||||
"""[N=1]""" | |||||
return self.call(method, int(n), **kwargs) | |||||
def autoscale(self, method, max=None, min=None, **kwargs): | |||||
"""[max] [min]""" | |||||
return self.call(method, int(max), int(min), **kwargs) | |||||
def rate_limit(self, method, task_name, rate_limit, **kwargs): | |||||
"""<task_name> <rate_limit> (e.g. 5/s | 5/m | 5/h)>""" | |||||
return self.call(method, task_name, rate_limit, **kwargs) | |||||
def time_limit(self, method, task_name, soft, hard=None, **kwargs): | |||||
"""<task_name> <soft_secs> [hard_secs]""" | |||||
return self.call(method, task_name, | |||||
float(soft), float(hard), **kwargs) | |||||
def add_consumer(self, method, queue, exchange=None, | |||||
exchange_type='direct', routing_key=None, **kwargs): | |||||
"""<queue> [exchange [type [routing_key]]]""" | |||||
return self.call(method, queue, exchange, | |||||
exchange_type, routing_key, **kwargs) | |||||
def cancel_consumer(self, method, queue, **kwargs): | |||||
"""<queue>""" | |||||
return self.call(method, queue, **kwargs) | |||||
class status(Command): | |||||
"""Show list of workers that are online.""" | |||||
option_list = inspect.option_list | |||||
def run(self, *args, **kwargs): | |||||
I = inspect( | |||||
app=self.app, | |||||
no_color=kwargs.get('no_color', False), | |||||
stdout=self.stdout, stderr=self.stderr, | |||||
show_reply=False, show_body=False, quiet=True, | |||||
) | |||||
replies = I.run('ping', **kwargs) | |||||
if not replies: | |||||
raise self.Error('No nodes replied within time constraint', | |||||
status=EX_UNAVAILABLE) | |||||
nodecount = len(replies) | |||||
if not kwargs.get('quiet', False): | |||||
self.out('\n{0} {1} online.'.format( | |||||
nodecount, text.pluralize(nodecount, 'node'))) | |||||
class migrate(Command): | |||||
"""Migrate tasks from one broker to another. | |||||
Examples:: | |||||
celery migrate redis://localhost amqp://guest@localhost// | |||||
celery migrate django:// redis://localhost | |||||
NOTE: This command is experimental, make sure you have | |||||
a backup of the tasks before you continue. | |||||
""" | |||||
args = '<source_url> <dest_url>' | |||||
option_list = Command.option_list + ( | |||||
Option('--limit', '-n', type='int', | |||||
help='Number of tasks to consume (int)'), | |||||
Option('--timeout', '-t', type='float', default=1.0, | |||||
help='Timeout in seconds (float) waiting for tasks'), | |||||
Option('--ack-messages', '-a', action='store_true', | |||||
help='Ack messages from source broker.'), | |||||
Option('--tasks', '-T', | |||||
help='List of task names to filter on.'), | |||||
Option('--queues', '-Q', | |||||
help='List of queues to migrate.'), | |||||
Option('--forever', '-F', action='store_true', | |||||
help='Continually migrate tasks until killed.'), | |||||
) | |||||
progress_fmt = MIGRATE_PROGRESS_FMT | |||||
def on_migrate_task(self, state, body, message): | |||||
self.out(self.progress_fmt.format(state=state, body=body)) | |||||
def run(self, source, destination, **kwargs): | |||||
from kombu import Connection | |||||
from celery.contrib.migrate import migrate_tasks | |||||
migrate_tasks(Connection(source), | |||||
Connection(destination), | |||||
callback=self.on_migrate_task, | |||||
**kwargs) | |||||
class shell(Command): # pragma: no cover | |||||
"""Start shell session with convenient access to celery symbols. | |||||
The following symbols will be added to the main globals: | |||||
- celery: the current application. | |||||
- chord, group, chain, chunks, | |||||
xmap, xstarmap subtask, Task | |||||
- all registered tasks. | |||||
""" | |||||
option_list = Command.option_list + ( | |||||
Option('--ipython', '-I', | |||||
action='store_true', dest='force_ipython', | |||||
help='force iPython.'), | |||||
Option('--bpython', '-B', | |||||
action='store_true', dest='force_bpython', | |||||
help='force bpython.'), | |||||
Option('--python', '-P', | |||||
action='store_true', dest='force_python', | |||||
help='force default Python shell.'), | |||||
Option('--without-tasks', '-T', action='store_true', | |||||
help="don't add tasks to locals."), | |||||
Option('--eventlet', action='store_true', | |||||
help='use eventlet.'), | |||||
Option('--gevent', action='store_true', help='use gevent.'), | |||||
) | |||||
def run(self, force_ipython=False, force_bpython=False, | |||||
force_python=False, without_tasks=False, eventlet=False, | |||||
gevent=False, **kwargs): | |||||
sys.path.insert(0, os.getcwd()) | |||||
if eventlet: | |||||
import_module('celery.concurrency.eventlet') | |||||
if gevent: | |||||
import_module('celery.concurrency.gevent') | |||||
import celery | |||||
import celery.task.base | |||||
self.app.loader.import_default_modules() | |||||
self.locals = {'app': self.app, | |||||
'celery': self.app, | |||||
'Task': celery.Task, | |||||
'chord': celery.chord, | |||||
'group': celery.group, | |||||
'chain': celery.chain, | |||||
'chunks': celery.chunks, | |||||
'xmap': celery.xmap, | |||||
'xstarmap': celery.xstarmap, | |||||
'subtask': celery.subtask, | |||||
'signature': celery.signature} | |||||
if not without_tasks: | |||||
self.locals.update(dict( | |||||
(task.__name__, task) for task in values(self.app.tasks) | |||||
if not task.name.startswith('celery.')), | |||||
) | |||||
if force_python: | |||||
return self.invoke_fallback_shell() | |||||
elif force_bpython: | |||||
return self.invoke_bpython_shell() | |||||
elif force_ipython: | |||||
return self.invoke_ipython_shell() | |||||
return self.invoke_default_shell() | |||||
def invoke_default_shell(self): | |||||
try: | |||||
import IPython # noqa | |||||
except ImportError: | |||||
try: | |||||
import bpython # noqa | |||||
except ImportError: | |||||
return self.invoke_fallback_shell() | |||||
else: | |||||
return self.invoke_bpython_shell() | |||||
else: | |||||
return self.invoke_ipython_shell() | |||||
def invoke_fallback_shell(self): | |||||
import code | |||||
try: | |||||
import readline | |||||
except ImportError: | |||||
pass | |||||
else: | |||||
import rlcompleter | |||||
readline.set_completer( | |||||
rlcompleter.Completer(self.locals).complete) | |||||
readline.parse_and_bind('tab:complete') | |||||
code.interact(local=self.locals) | |||||
def invoke_ipython_shell(self): | |||||
for ip in (self._ipython, self._ipython_pre_10, | |||||
self._ipython_terminal, self._ipython_010, | |||||
self._no_ipython): | |||||
try: | |||||
return ip() | |||||
except ImportError: | |||||
pass | |||||
def _ipython(self): | |||||
from IPython import start_ipython | |||||
start_ipython(argv=[], user_ns=self.locals) | |||||
def _ipython_pre_10(self): # pragma: no cover | |||||
from IPython.frontend.terminal.ipapp import TerminalIPythonApp | |||||
app = TerminalIPythonApp.instance() | |||||
app.initialize(argv=[]) | |||||
app.shell.user_ns.update(self.locals) | |||||
app.start() | |||||
def _ipython_terminal(self): # pragma: no cover | |||||
from IPython.terminal import embed | |||||
embed.TerminalInteractiveShell(user_ns=self.locals).mainloop() | |||||
def _ipython_010(self): # pragma: no cover | |||||
from IPython.Shell import IPShell | |||||
IPShell(argv=[], user_ns=self.locals).mainloop() | |||||
def _no_ipython(self): # pragma: no cover | |||||
raise ImportError("no suitable ipython found") | |||||
def invoke_bpython_shell(self): | |||||
import bpython | |||||
bpython.embed(self.locals) | |||||
class help(Command): | |||||
"""Show help screen and exit.""" | |||||
def usage(self, command): | |||||
return '%prog <command> [options] {0.args}'.format(self) | |||||
def run(self, *args, **kwargs): | |||||
self.parser.print_help() | |||||
self.out(HELP.format( | |||||
prog_name=self.prog_name, | |||||
commands=CeleryCommand.list_commands(colored=self.colored), | |||||
)) | |||||
return EX_USAGE | |||||
class report(Command): | |||||
"""Shows information useful to include in bugreports.""" | |||||
def run(self, *args, **kwargs): | |||||
self.out(self.app.bugreport()) | |||||
return EX_OK | |||||
class CeleryCommand(Command): | |||||
namespace = 'celery' | |||||
ext_fmt = '{self.namespace}.commands' | |||||
commands = { | |||||
'amqp': amqp, | |||||
'beat': beat, | |||||
'call': call, | |||||
'control': control, | |||||
'events': events, | |||||
'graph': graph, | |||||
'help': help, | |||||
'inspect': inspect, | |||||
'list': list_, | |||||
'migrate': migrate, | |||||
'multi': multi, | |||||
'purge': purge, | |||||
'report': report, | |||||
'result': result, | |||||
'shell': shell, | |||||
'status': status, | |||||
'worker': worker, | |||||
} | |||||
enable_config_from_cmdline = True | |||||
prog_name = 'celery' | |||||
@classmethod | |||||
def register_command(cls, fun, name=None): | |||||
cls.commands[name or fun.__name__] = fun | |||||
return fun | |||||
def execute(self, command, argv=None): | |||||
try: | |||||
cls = self.commands[command] | |||||
except KeyError: | |||||
cls, argv = self.commands['help'], ['help'] | |||||
cls = self.commands.get(command) or self.commands['help'] | |||||
try: | |||||
return cls( | |||||
app=self.app, on_error=self.on_error, | |||||
no_color=self.no_color, quiet=self.quiet, | |||||
on_usage_error=partial(self.on_usage_error, command=command), | |||||
).run_from_argv(self.prog_name, argv[1:], command=argv[0]) | |||||
except self.UsageError as exc: | |||||
self.on_usage_error(exc) | |||||
return exc.status | |||||
except self.Error as exc: | |||||
self.on_error(exc) | |||||
return exc.status | |||||
def on_usage_error(self, exc, command=None): | |||||
if command: | |||||
helps = '{self.prog_name} {command} --help' | |||||
else: | |||||
helps = '{self.prog_name} --help' | |||||
self.error(self.colored.magenta('Error: {0}'.format(exc))) | |||||
self.error("""Please try '{0}'""".format(helps.format( | |||||
self=self, command=command, | |||||
))) | |||||
def _relocate_args_from_start(self, argv, index=0): | |||||
if argv: | |||||
rest = [] | |||||
while index < len(argv): | |||||
value = argv[index] | |||||
if value.startswith('--'): | |||||
rest.append(value) | |||||
elif value.startswith('-'): | |||||
# we eat the next argument even though we don't know | |||||
# if this option takes an argument or not. | |||||
# instead we will assume what is the command name in the | |||||
# return statements below. | |||||
try: | |||||
nxt = argv[index + 1] | |||||
if nxt.startswith('-'): | |||||
# is another option | |||||
rest.append(value) | |||||
else: | |||||
# is (maybe) a value for this option | |||||
rest.extend([value, nxt]) | |||||
index += 1 | |||||
except IndexError: | |||||
rest.append(value) | |||||
break | |||||
else: | |||||
break | |||||
index += 1 | |||||
if argv[index:]: | |||||
# if there are more arguments left then divide and swap | |||||
# we assume the first argument in argv[i:] is the command | |||||
# name. | |||||
return argv[index:] + rest | |||||
# if there are no more arguments then the last arg in rest' | |||||
# must be the command. | |||||
[rest.pop()] + rest | |||||
return [] | |||||
def prepare_prog_name(self, name): | |||||
if name == '__main__.py': | |||||
return sys.modules['__main__'].__file__ | |||||
return name | |||||
def handle_argv(self, prog_name, argv): | |||||
self.prog_name = self.prepare_prog_name(prog_name) | |||||
argv = self._relocate_args_from_start(argv) | |||||
_, argv = self.prepare_args(None, argv) | |||||
try: | |||||
command = argv[0] | |||||
except IndexError: | |||||
command, argv = 'help', ['help'] | |||||
return self.execute(command, argv) | |||||
def execute_from_commandline(self, argv=None): | |||||
argv = sys.argv if argv is None else argv | |||||
if 'multi' in argv[1:3]: # Issue 1008 | |||||
self.respects_app_option = False | |||||
try: | |||||
sys.exit(determine_exit_status( | |||||
super(CeleryCommand, self).execute_from_commandline(argv))) | |||||
except KeyboardInterrupt: | |||||
sys.exit(EX_FAILURE) | |||||
@classmethod | |||||
def get_command_info(self, command, indent=0, color=None, colored=None): | |||||
colored = term.colored() if colored is None else colored | |||||
colored = colored.names[color] if color else lambda x: x | |||||
obj = self.commands[command] | |||||
cmd = 'celery {0}'.format(colored(command)) | |||||
if obj.leaf: | |||||
return '|' + text.indent(cmd, indent) | |||||
return text.join([ | |||||
' ', | |||||
'|' + text.indent('{0} --help'.format(cmd), indent), | |||||
obj.list_commands(indent, 'celery {0}'.format(command), colored), | |||||
]) | |||||
@classmethod | |||||
def list_commands(self, indent=0, colored=None): | |||||
colored = term.colored() if colored is None else colored | |||||
white = colored.white | |||||
ret = [] | |||||
for cls, commands, color in command_classes: | |||||
ret.extend([ | |||||
text.indent('+ {0}: '.format(white(cls)), indent), | |||||
'\n'.join( | |||||
self.get_command_info(command, indent + 4, color, colored) | |||||
for command in commands), | |||||
'' | |||||
]) | |||||
return '\n'.join(ret).strip() | |||||
def with_pool_option(self, argv): | |||||
if len(argv) > 1 and 'worker' in argv[0:3]: | |||||
# this command supports custom pools | |||||
# that may have to be loaded as early as possible. | |||||
return (['-P'], ['--pool']) | |||||
def on_concurrency_setup(self): | |||||
self.load_extension_commands() | |||||
def load_extension_commands(self): | |||||
names = Extensions(self.ext_fmt.format(self=self), | |||||
self.register_command).load() | |||||
if names: | |||||
command_classes.append(('Extensions', names, 'magenta')) | |||||
def command(*args, **kwargs): | |||||
"""Deprecated: Use classmethod :meth:`CeleryCommand.register_command` | |||||
instead.""" | |||||
_register = CeleryCommand.register_command | |||||
return _register(args[0]) if args else _register | |||||
if __name__ == '__main__': # pragma: no cover | |||||
main() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.bin.celeryd_detach | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Program used to daemonize the worker | |||||
Using :func:`os.execv` because forking and multiprocessing | |||||
leads to weird issues (it was a long time ago now, but it | |||||
could have something to do with the threading mutex bug) | |||||
""" | |||||
from __future__ import absolute_import | |||||
import celery | |||||
import os | |||||
import sys | |||||
from optparse import OptionParser, BadOptionError | |||||
from celery.platforms import EX_FAILURE, detached | |||||
from celery.utils import default_nodename, node_format | |||||
from celery.utils.log import get_logger | |||||
from celery.bin.base import daemon_options, Option | |||||
__all__ = ['detached_celeryd', 'detach'] | |||||
logger = get_logger(__name__) | |||||
C_FAKEFORK = os.environ.get('C_FAKEFORK') | |||||
OPTION_LIST = daemon_options(default_pidfile='celeryd.pid') + ( | |||||
Option('--workdir', default=None, dest='working_directory'), | |||||
Option('-n', '--hostname'), | |||||
Option('--fake', | |||||
default=False, action='store_true', dest='fake', | |||||
help="Don't fork (for debugging purposes)"), | |||||
) | |||||
def detach(path, argv, logfile=None, pidfile=None, uid=None, | |||||
gid=None, umask=None, working_directory=None, fake=False, app=None, | |||||
executable=None, hostname=None): | |||||
hostname = default_nodename(hostname) | |||||
logfile = node_format(logfile, hostname) | |||||
pidfile = node_format(pidfile, hostname) | |||||
fake = 1 if C_FAKEFORK else fake | |||||
with detached(logfile, pidfile, uid, gid, umask, working_directory, fake, | |||||
after_forkers=False): | |||||
try: | |||||
if executable is not None: | |||||
path = executable | |||||
os.execv(path, [path] + argv) | |||||
except Exception: | |||||
if app is None: | |||||
from celery import current_app | |||||
app = current_app | |||||
app.log.setup_logging_subsystem( | |||||
'ERROR', logfile, hostname=hostname) | |||||
logger.critical("Can't exec %r", ' '.join([path] + argv), | |||||
exc_info=True) | |||||
return EX_FAILURE | |||||
class PartialOptionParser(OptionParser): | |||||
def __init__(self, *args, **kwargs): | |||||
self.leftovers = [] | |||||
OptionParser.__init__(self, *args, **kwargs) | |||||
def _process_long_opt(self, rargs, values): | |||||
arg = rargs.pop(0) | |||||
if '=' in arg: | |||||
opt, next_arg = arg.split('=', 1) | |||||
rargs.insert(0, next_arg) | |||||
had_explicit_value = True | |||||
else: | |||||
opt = arg | |||||
had_explicit_value = False | |||||
try: | |||||
opt = self._match_long_opt(opt) | |||||
option = self._long_opt.get(opt) | |||||
except BadOptionError: | |||||
option = None | |||||
if option: | |||||
if option.takes_value(): | |||||
nargs = option.nargs | |||||
if len(rargs) < nargs: | |||||
if nargs == 1: | |||||
self.error('{0} requires an argument'.format(opt)) | |||||
else: | |||||
self.error('{0} requires {1} arguments'.format( | |||||
opt, nargs)) | |||||
elif nargs == 1: | |||||
value = rargs.pop(0) | |||||
else: | |||||
value = tuple(rargs[0:nargs]) | |||||
del rargs[0:nargs] | |||||
elif had_explicit_value: | |||||
self.error('{0} option does not take a value'.format(opt)) | |||||
else: | |||||
value = None | |||||
option.process(opt, value, values, self) | |||||
else: | |||||
self.leftovers.append(arg) | |||||
def _process_short_opts(self, rargs, values): | |||||
arg = rargs[0] | |||||
try: | |||||
OptionParser._process_short_opts(self, rargs, values) | |||||
except BadOptionError: | |||||
self.leftovers.append(arg) | |||||
if rargs and not rargs[0][0] == '-': | |||||
self.leftovers.append(rargs.pop(0)) | |||||
class detached_celeryd(object): | |||||
option_list = OPTION_LIST | |||||
usage = '%prog [options] [celeryd options]' | |||||
version = celery.VERSION_BANNER | |||||
description = ('Detaches Celery worker nodes. See `celery worker --help` ' | |||||
'for the list of supported worker arguments.') | |||||
command = sys.executable | |||||
execv_path = sys.executable | |||||
if sys.version_info < (2, 7): # does not support pkg/__main__.py | |||||
execv_argv = ['-m', 'celery.__main__', 'worker'] | |||||
else: | |||||
execv_argv = ['-m', 'celery', 'worker'] | |||||
def __init__(self, app=None): | |||||
self.app = app | |||||
def Parser(self, prog_name): | |||||
return PartialOptionParser(prog=prog_name, | |||||
option_list=self.option_list, | |||||
usage=self.usage, | |||||
description=self.description, | |||||
version=self.version) | |||||
def parse_options(self, prog_name, argv): | |||||
parser = self.Parser(prog_name) | |||||
options, values = parser.parse_args(argv) | |||||
if options.logfile: | |||||
parser.leftovers.append('--logfile={0}'.format(options.logfile)) | |||||
if options.pidfile: | |||||
parser.leftovers.append('--pidfile={0}'.format(options.pidfile)) | |||||
if options.hostname: | |||||
parser.leftovers.append('--hostname={0}'.format(options.hostname)) | |||||
return options, values, parser.leftovers | |||||
def execute_from_commandline(self, argv=None): | |||||
if argv is None: | |||||
argv = sys.argv | |||||
config = [] | |||||
seen_cargs = 0 | |||||
for arg in argv: | |||||
if seen_cargs: | |||||
config.append(arg) | |||||
else: | |||||
if arg == '--': | |||||
seen_cargs = 1 | |||||
config.append(arg) | |||||
prog_name = os.path.basename(argv[0]) | |||||
options, values, leftovers = self.parse_options(prog_name, argv[1:]) | |||||
sys.exit(detach( | |||||
app=self.app, path=self.execv_path, | |||||
argv=self.execv_argv + leftovers + config, | |||||
**vars(options) | |||||
)) | |||||
def main(app=None): | |||||
detached_celeryd(app).execute_from_commandline() | |||||
if __name__ == '__main__': # pragma: no cover | |||||
main() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
The :program:`celery events` command. | |||||
.. program:: celery events | |||||
.. seealso:: | |||||
See :ref:`preload-options` and :ref:`daemon-options`. | |||||
.. cmdoption:: -d, --dump | |||||
Dump events to stdout. | |||||
.. cmdoption:: -c, --camera | |||||
Take snapshots of events using this camera. | |||||
.. cmdoption:: --detach | |||||
Camera: Detach and run in the background as a daemon. | |||||
.. cmdoption:: -F, --freq, --frequency | |||||
Camera: Shutter frequency. Default is every 1.0 seconds. | |||||
.. cmdoption:: -r, --maxrate | |||||
Camera: Optional shutter rate limit (e.g. 10/m). | |||||
.. cmdoption:: -l, --loglevel | |||||
Logging level, choose between `DEBUG`, `INFO`, `WARNING`, | |||||
`ERROR`, `CRITICAL`, or `FATAL`. Default is INFO. | |||||
""" | |||||
from __future__ import absolute_import, unicode_literals | |||||
import sys | |||||
from functools import partial | |||||
from celery.platforms import detached, set_process_title, strargv | |||||
from celery.bin.base import Command, Option, daemon_options | |||||
__all__ = ['events'] | |||||
class events(Command): | |||||
"""Event-stream utilities. | |||||
Commands:: | |||||
celery events --app=proj | |||||
start graphical monitor (requires curses) | |||||
celery events -d --app=proj | |||||
dump events to screen. | |||||
celery events -b amqp:// | |||||
celery events -c <camera> [options] | |||||
run snapshot camera. | |||||
Examples:: | |||||
celery events | |||||
celery events -d | |||||
celery events -c mod.attr -F 1.0 --detach --maxrate=100/m -l info | |||||
""" | |||||
doc = __doc__ | |||||
supports_args = False | |||||
def run(self, dump=False, camera=None, frequency=1.0, maxrate=None, | |||||
loglevel='INFO', logfile=None, prog_name='celery events', | |||||
pidfile=None, uid=None, gid=None, umask=None, | |||||
working_directory=None, detach=False, **kwargs): | |||||
self.prog_name = prog_name | |||||
if dump: | |||||
return self.run_evdump() | |||||
if camera: | |||||
return self.run_evcam(camera, freq=frequency, maxrate=maxrate, | |||||
loglevel=loglevel, logfile=logfile, | |||||
pidfile=pidfile, uid=uid, gid=gid, | |||||
umask=umask, | |||||
working_directory=working_directory, | |||||
detach=detach) | |||||
return self.run_evtop() | |||||
def run_evdump(self): | |||||
from celery.events.dumper import evdump | |||||
self.set_process_status('dump') | |||||
return evdump(app=self.app) | |||||
def run_evtop(self): | |||||
from celery.events.cursesmon import evtop | |||||
self.set_process_status('top') | |||||
return evtop(app=self.app) | |||||
def run_evcam(self, camera, logfile=None, pidfile=None, uid=None, | |||||
gid=None, umask=None, working_directory=None, | |||||
detach=False, **kwargs): | |||||
from celery.events.snapshot import evcam | |||||
workdir = working_directory | |||||
self.set_process_status('cam') | |||||
kwargs['app'] = self.app | |||||
cam = partial(evcam, camera, | |||||
logfile=logfile, pidfile=pidfile, **kwargs) | |||||
if detach: | |||||
with detached(logfile, pidfile, uid, gid, umask, workdir): | |||||
return cam() | |||||
else: | |||||
return cam() | |||||
def set_process_status(self, prog, info=''): | |||||
prog = '{0}:{1}'.format(self.prog_name, prog) | |||||
info = '{0} {1}'.format(info, strargv(sys.argv)) | |||||
return set_process_title(prog, info=info) | |||||
def get_options(self): | |||||
return ( | |||||
(Option('-d', '--dump', action='store_true'), | |||||
Option('-c', '--camera'), | |||||
Option('--detach', action='store_true'), | |||||
Option('-F', '--frequency', '--freq', | |||||
type='float', default=1.0), | |||||
Option('-r', '--maxrate'), | |||||
Option('-l', '--loglevel', default='INFO')) + | |||||
daemon_options(default_pidfile='celeryev.pid') + | |||||
tuple(self.app.user_options['events']) | |||||
) | |||||
def main(): | |||||
ev = events() | |||||
ev.execute_from_commandline() | |||||
if __name__ == '__main__': # pragma: no cover | |||||
main() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
The :program:`celery graph` command. | |||||
.. program:: celery graph | |||||
""" | |||||
from __future__ import absolute_import, unicode_literals | |||||
from operator import itemgetter | |||||
from celery.datastructures import DependencyGraph, GraphFormatter | |||||
from celery.five import items | |||||
from .base import Command | |||||
__all__ = ['graph'] | |||||
class graph(Command): | |||||
args = """<TYPE> [arguments] | |||||
..... bootsteps [worker] [consumer] | |||||
..... workers [enumerate] | |||||
""" | |||||
def run(self, what=None, *args, **kwargs): | |||||
map = {'bootsteps': self.bootsteps, 'workers': self.workers} | |||||
if not what: | |||||
raise self.UsageError('missing type') | |||||
elif what not in map: | |||||
raise self.Error('no graph {0} in {1}'.format(what, '|'.join(map))) | |||||
return map[what](*args, **kwargs) | |||||
def bootsteps(self, *args, **kwargs): | |||||
worker = self.app.WorkController() | |||||
include = set(arg.lower() for arg in args or ['worker', 'consumer']) | |||||
if 'worker' in include: | |||||
graph = worker.blueprint.graph | |||||
if 'consumer' in include: | |||||
worker.blueprint.connect_with(worker.consumer.blueprint) | |||||
else: | |||||
graph = worker.consumer.blueprint.graph | |||||
graph.to_dot(self.stdout) | |||||
def workers(self, *args, **kwargs): | |||||
def simplearg(arg): | |||||
return maybe_list(itemgetter(0, 2)(arg.partition(':'))) | |||||
def maybe_list(l, sep=','): | |||||
return (l[0], l[1].split(sep) if sep in l[1] else l[1]) | |||||
args = dict(simplearg(arg) for arg in args) | |||||
generic = 'generic' in args | |||||
def generic_label(node): | |||||
return '{0} ({1}://)'.format(type(node).__name__, | |||||
node._label.split('://')[0]) | |||||
class Node(object): | |||||
force_label = None | |||||
scheme = {} | |||||
def __init__(self, label, pos=None): | |||||
self._label = label | |||||
self.pos = pos | |||||
def label(self): | |||||
return self._label | |||||
def __str__(self): | |||||
return self.label() | |||||
class Thread(Node): | |||||
scheme = {'fillcolor': 'lightcyan4', 'fontcolor': 'yellow', | |||||
'shape': 'oval', 'fontsize': 10, 'width': 0.3, | |||||
'color': 'black'} | |||||
def __init__(self, label, **kwargs): | |||||
self._label = 'thr-{0}'.format(next(tids)) | |||||
self.real_label = label | |||||
self.pos = 0 | |||||
class Formatter(GraphFormatter): | |||||
def label(self, obj): | |||||
return obj and obj.label() | |||||
def node(self, obj): | |||||
scheme = dict(obj.scheme) if obj.pos else obj.scheme | |||||
if isinstance(obj, Thread): | |||||
scheme['label'] = obj.real_label | |||||
return self.draw_node( | |||||
obj, dict(self.node_scheme, **scheme), | |||||
) | |||||
def terminal_node(self, obj): | |||||
return self.draw_node( | |||||
obj, dict(self.term_scheme, **obj.scheme), | |||||
) | |||||
def edge(self, a, b, **attrs): | |||||
if isinstance(a, Thread): | |||||
attrs.update(arrowhead='none', arrowtail='tee') | |||||
return self.draw_edge(a, b, self.edge_scheme, attrs) | |||||
def subscript(n): | |||||
S = {'0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', | |||||
'5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉'} | |||||
return ''.join([S[i] for i in str(n)]) | |||||
class Worker(Node): | |||||
pass | |||||
class Backend(Node): | |||||
scheme = {'shape': 'folder', 'width': 2, | |||||
'height': 1, 'color': 'black', | |||||
'fillcolor': 'peachpuff3', 'color': 'peachpuff4'} | |||||
def label(self): | |||||
return generic_label(self) if generic else self._label | |||||
class Broker(Node): | |||||
scheme = {'shape': 'circle', 'fillcolor': 'cadetblue3', | |||||
'color': 'cadetblue4', 'height': 1} | |||||
def label(self): | |||||
return generic_label(self) if generic else self._label | |||||
from itertools import count | |||||
tids = count(1) | |||||
Wmax = int(args.get('wmax', 4) or 0) | |||||
Tmax = int(args.get('tmax', 3) or 0) | |||||
def maybe_abbr(l, name, max=Wmax): | |||||
size = len(l) | |||||
abbr = max and size > max | |||||
if 'enumerate' in args: | |||||
l = ['{0}{1}'.format(name, subscript(i + 1)) | |||||
for i, obj in enumerate(l)] | |||||
if abbr: | |||||
l = l[0:max - 1] + [l[size - 1]] | |||||
l[max - 2] = '{0}⎨…{1}⎬'.format( | |||||
name[0], subscript(size - (max - 1))) | |||||
return l | |||||
try: | |||||
workers = args['nodes'] | |||||
threads = args.get('threads') or [] | |||||
except KeyError: | |||||
replies = self.app.control.inspect().stats() | |||||
workers, threads = [], [] | |||||
for worker, reply in items(replies): | |||||
workers.append(worker) | |||||
threads.append(reply['pool']['max-concurrency']) | |||||
wlen = len(workers) | |||||
backend = args.get('backend', self.app.conf.CELERY_RESULT_BACKEND) | |||||
threads_for = {} | |||||
workers = maybe_abbr(workers, 'Worker') | |||||
if Wmax and wlen > Wmax: | |||||
threads = threads[0:3] + [threads[-1]] | |||||
for i, threads in enumerate(threads): | |||||
threads_for[workers[i]] = maybe_abbr( | |||||
list(range(int(threads))), 'P', Tmax, | |||||
) | |||||
broker = Broker(args.get('broker', self.app.connection().as_uri())) | |||||
backend = Backend(backend) if backend else None | |||||
graph = DependencyGraph(formatter=Formatter()) | |||||
graph.add_arc(broker) | |||||
if backend: | |||||
graph.add_arc(backend) | |||||
curworker = [0] | |||||
for i, worker in enumerate(workers): | |||||
worker = Worker(worker, pos=i) | |||||
graph.add_arc(worker) | |||||
graph.add_edge(worker, broker) | |||||
if backend: | |||||
graph.add_edge(worker, backend) | |||||
threads = threads_for.get(worker._label) | |||||
if threads: | |||||
for thread in threads: | |||||
thread = Thread(thread) | |||||
graph.add_arc(thread) | |||||
graph.add_edge(thread, worker) | |||||
curworker[0] += 1 | |||||
graph.to_dot(self.stdout) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
.. program:: celery multi | |||||
Examples | |||||
======== | |||||
.. code-block:: bash | |||||
# Single worker with explicit name and events enabled. | |||||
$ celery multi start Leslie -E | |||||
# Pidfiles and logfiles are stored in the current directory | |||||
# by default. Use --pidfile and --logfile argument to change | |||||
# this. The abbreviation %N will be expanded to the current | |||||
# node name. | |||||
$ celery multi start Leslie -E --pidfile=/var/run/celery/%N.pid | |||||
--logfile=/var/log/celery/%N.log | |||||
# You need to add the same arguments when you restart, | |||||
# as these are not persisted anywhere. | |||||
$ celery multi restart Leslie -E --pidfile=/var/run/celery/%N.pid | |||||
--logfile=/var/run/celery/%N.log | |||||
# To stop the node, you need to specify the same pidfile. | |||||
$ celery multi stop Leslie --pidfile=/var/run/celery/%N.pid | |||||
# 3 workers, with 3 processes each | |||||
$ celery multi start 3 -c 3 | |||||
celery worker -n celery1@myhost -c 3 | |||||
celery worker -n celery2@myhost -c 3 | |||||
celery worker -n celery3@myhost -c 3 | |||||
# start 3 named workers | |||||
$ celery multi start image video data -c 3 | |||||
celery worker -n image@myhost -c 3 | |||||
celery worker -n video@myhost -c 3 | |||||
celery worker -n data@myhost -c 3 | |||||
# specify custom hostname | |||||
$ celery multi start 2 --hostname=worker.example.com -c 3 | |||||
celery worker -n celery1@worker.example.com -c 3 | |||||
celery worker -n celery2@worker.example.com -c 3 | |||||
# specify fully qualified nodenames | |||||
$ celery multi start foo@worker.example.com bar@worker.example.com -c 3 | |||||
# Advanced example starting 10 workers in the background: | |||||
# * Three of the workers processes the images and video queue | |||||
# * Two of the workers processes the data queue with loglevel DEBUG | |||||
# * the rest processes the default' queue. | |||||
$ celery multi start 10 -l INFO -Q:1-3 images,video -Q:4,5 data | |||||
-Q default -L:4,5 DEBUG | |||||
# You can show the commands necessary to start the workers with | |||||
# the 'show' command: | |||||
$ celery multi show 10 -l INFO -Q:1-3 images,video -Q:4,5 data | |||||
-Q default -L:4,5 DEBUG | |||||
# Additional options are added to each celery worker' comamnd, | |||||
# but you can also modify the options for ranges of, or specific workers | |||||
# 3 workers: Two with 3 processes, and one with 10 processes. | |||||
$ celery multi start 3 -c 3 -c:1 10 | |||||
celery worker -n celery1@myhost -c 10 | |||||
celery worker -n celery2@myhost -c 3 | |||||
celery worker -n celery3@myhost -c 3 | |||||
# can also specify options for named workers | |||||
$ celery multi start image video data -c 3 -c:image 10 | |||||
celery worker -n image@myhost -c 10 | |||||
celery worker -n video@myhost -c 3 | |||||
celery worker -n data@myhost -c 3 | |||||
# ranges and lists of workers in options is also allowed: | |||||
# (-c:1-3 can also be written as -c:1,2,3) | |||||
$ celery multi start 5 -c 3 -c:1-3 10 | |||||
celery worker -n celery1@myhost -c 10 | |||||
celery worker -n celery2@myhost -c 10 | |||||
celery worker -n celery3@myhost -c 10 | |||||
celery worker -n celery4@myhost -c 3 | |||||
celery worker -n celery5@myhost -c 3 | |||||
# lists also works with named workers | |||||
$ celery multi start foo bar baz xuzzy -c 3 -c:foo,bar,baz 10 | |||||
celery worker -n foo@myhost -c 10 | |||||
celery worker -n bar@myhost -c 10 | |||||
celery worker -n baz@myhost -c 10 | |||||
celery worker -n xuzzy@myhost -c 3 | |||||
""" | |||||
from __future__ import absolute_import, print_function, unicode_literals | |||||
import errno | |||||
import os | |||||
import shlex | |||||
import signal | |||||
import socket | |||||
import sys | |||||
from collections import defaultdict, namedtuple | |||||
from subprocess import Popen | |||||
from time import sleep | |||||
from kombu.utils import cached_property | |||||
from kombu.utils.compat import OrderedDict | |||||
from kombu.utils.encoding import from_utf8 | |||||
from celery import VERSION_BANNER | |||||
from celery.five import items | |||||
from celery.platforms import Pidfile, IS_WINDOWS | |||||
from celery.utils import term, nodesplit | |||||
from celery.utils.text import pluralize | |||||
__all__ = ['MultiTool'] | |||||
SIGNAMES = set(sig for sig in dir(signal) | |||||
if sig.startswith('SIG') and '_' not in sig) | |||||
SIGMAP = dict((getattr(signal, name), name) for name in SIGNAMES) | |||||
USAGE = """\ | |||||
usage: {prog_name} start <node1 node2 nodeN|range> [worker options] | |||||
{prog_name} stop <n1 n2 nN|range> [-SIG (default: -TERM)] | |||||
{prog_name} stopwait <n1 n2 nN|range> [-SIG (default: -TERM)] | |||||
{prog_name} restart <n1 n2 nN|range> [-SIG] [worker options] | |||||
{prog_name} kill <n1 n2 nN|range> | |||||
{prog_name} show <n1 n2 nN|range> [worker options] | |||||
{prog_name} get hostname <n1 n2 nN|range> [-qv] [worker options] | |||||
{prog_name} names <n1 n2 nN|range> | |||||
{prog_name} expand template <n1 n2 nN|range> | |||||
{prog_name} help | |||||
additional options (must appear after command name): | |||||
* --nosplash: Don't display program info. | |||||
* --quiet: Don't show as much output. | |||||
* --verbose: Show more output. | |||||
* --no-color: Don't display colors. | |||||
""" | |||||
multi_args_t = namedtuple( | |||||
'multi_args_t', ('name', 'argv', 'expander', 'namespace'), | |||||
) | |||||
def main(): | |||||
sys.exit(MultiTool().execute_from_commandline(sys.argv)) | |||||
CELERY_EXE = 'celery' | |||||
if sys.version_info < (2, 7): | |||||
# pkg.__main__ first supported in Py2.7 | |||||
CELERY_EXE = 'celery.__main__' | |||||
def celery_exe(*args): | |||||
return ' '.join((CELERY_EXE, ) + args) | |||||
class MultiTool(object): | |||||
retcode = 0 # Final exit code. | |||||
def __init__(self, env=None, fh=None, quiet=False, verbose=False, | |||||
no_color=False, nosplash=False, stdout=None, stderr=None): | |||||
"""fh is an old alias to stdout.""" | |||||
self.stdout = self.fh = stdout or fh or sys.stdout | |||||
self.stderr = stderr or sys.stderr | |||||
self.env = env | |||||
self.nosplash = nosplash | |||||
self.quiet = quiet | |||||
self.verbose = verbose | |||||
self.no_color = no_color | |||||
self.prog_name = 'celery multi' | |||||
self.commands = {'start': self.start, | |||||
'show': self.show, | |||||
'stop': self.stop, | |||||
'stopwait': self.stopwait, | |||||
'stop_verify': self.stopwait, # compat alias | |||||
'restart': self.restart, | |||||
'kill': self.kill, | |||||
'names': self.names, | |||||
'expand': self.expand, | |||||
'get': self.get, | |||||
'help': self.help} | |||||
def execute_from_commandline(self, argv, cmd='celery worker'): | |||||
argv = list(argv) # don't modify callers argv. | |||||
# Reserve the --nosplash|--quiet|-q/--verbose options. | |||||
if '--nosplash' in argv: | |||||
self.nosplash = argv.pop(argv.index('--nosplash')) | |||||
if '--quiet' in argv: | |||||
self.quiet = argv.pop(argv.index('--quiet')) | |||||
if '-q' in argv: | |||||
self.quiet = argv.pop(argv.index('-q')) | |||||
if '--verbose' in argv: | |||||
self.verbose = argv.pop(argv.index('--verbose')) | |||||
if '--no-color' in argv: | |||||
self.no_color = argv.pop(argv.index('--no-color')) | |||||
self.prog_name = os.path.basename(argv.pop(0)) | |||||
if not argv or argv[0][0] == '-': | |||||
return self.error() | |||||
try: | |||||
self.commands[argv[0]](argv[1:], cmd) | |||||
except KeyError: | |||||
self.error('Invalid command: {0}'.format(argv[0])) | |||||
return self.retcode | |||||
def say(self, m, newline=True, file=None): | |||||
print(m, file=file or self.stdout, end='\n' if newline else '') | |||||
def carp(self, m, newline=True, file=None): | |||||
return self.say(m, newline, file or self.stderr) | |||||
def names(self, argv, cmd): | |||||
p = NamespacedOptionParser(argv) | |||||
self.say('\n'.join( | |||||
n.name for n in multi_args(p, cmd)), | |||||
) | |||||
def get(self, argv, cmd): | |||||
wanted = argv[0] | |||||
p = NamespacedOptionParser(argv[1:]) | |||||
for node in multi_args(p, cmd): | |||||
if node.name == wanted: | |||||
self.say(' '.join(node.argv)) | |||||
return | |||||
def show(self, argv, cmd): | |||||
p = NamespacedOptionParser(argv) | |||||
self.with_detacher_default_options(p) | |||||
self.say('\n'.join( | |||||
' '.join([sys.executable] + n.argv) for n in multi_args(p, cmd)), | |||||
) | |||||
def start(self, argv, cmd): | |||||
self.splash() | |||||
p = NamespacedOptionParser(argv) | |||||
self.with_detacher_default_options(p) | |||||
retcodes = [] | |||||
self.note('> Starting nodes...') | |||||
for node in multi_args(p, cmd): | |||||
self.note('\t> {0}: '.format(node.name), newline=False) | |||||
retcode = self.waitexec(node.argv, path=p.options['--executable']) | |||||
self.note(retcode and self.FAILED or self.OK) | |||||
retcodes.append(retcode) | |||||
self.retcode = int(any(retcodes)) | |||||
def with_detacher_default_options(self, p): | |||||
_setdefaultopt(p.options, ['--pidfile', '-p'], '%N.pid') | |||||
_setdefaultopt(p.options, ['--logfile', '-f'], '%N.log') | |||||
p.options.setdefault( | |||||
'--cmd', | |||||
'-m {0}'.format(celery_exe('worker', '--detach')), | |||||
) | |||||
_setdefaultopt(p.options, ['--executable'], sys.executable) | |||||
def signal_node(self, nodename, pid, sig): | |||||
try: | |||||
os.kill(pid, sig) | |||||
except OSError as exc: | |||||
if exc.errno != errno.ESRCH: | |||||
raise | |||||
self.note('Could not signal {0} ({1}): No such process'.format( | |||||
nodename, pid)) | |||||
return False | |||||
return True | |||||
def node_alive(self, pid): | |||||
try: | |||||
os.kill(pid, 0) | |||||
except OSError as exc: | |||||
if exc.errno == errno.ESRCH: | |||||
return False | |||||
raise | |||||
return True | |||||
def shutdown_nodes(self, nodes, sig=signal.SIGTERM, retry=None, | |||||
callback=None): | |||||
if not nodes: | |||||
return | |||||
P = set(nodes) | |||||
def on_down(node): | |||||
P.discard(node) | |||||
if callback: | |||||
callback(*node) | |||||
self.note(self.colored.blue('> Stopping nodes...')) | |||||
for node in list(P): | |||||
if node in P: | |||||
nodename, _, pid = node | |||||
self.note('\t> {0}: {1} -> {2}'.format( | |||||
nodename, SIGMAP[sig][3:], pid)) | |||||
if not self.signal_node(nodename, pid, sig): | |||||
on_down(node) | |||||
def note_waiting(): | |||||
left = len(P) | |||||
if left: | |||||
pids = ', '.join(str(pid) for _, _, pid in P) | |||||
self.note(self.colored.blue( | |||||
'> Waiting for {0} {1} -> {2}...'.format( | |||||
left, pluralize(left, 'node'), pids)), newline=False) | |||||
if retry: | |||||
note_waiting() | |||||
its = 0 | |||||
while P: | |||||
for node in P: | |||||
its += 1 | |||||
self.note('.', newline=False) | |||||
nodename, _, pid = node | |||||
if not self.node_alive(pid): | |||||
self.note('\n\t> {0}: {1}'.format(nodename, self.OK)) | |||||
on_down(node) | |||||
note_waiting() | |||||
break | |||||
if P and not its % len(P): | |||||
sleep(float(retry)) | |||||
self.note('') | |||||
def getpids(self, p, cmd, callback=None): | |||||
_setdefaultopt(p.options, ['--pidfile', '-p'], '%N.pid') | |||||
nodes = [] | |||||
for node in multi_args(p, cmd): | |||||
try: | |||||
pidfile_template = _getopt( | |||||
p.namespaces[node.namespace], ['--pidfile', '-p'], | |||||
) | |||||
except KeyError: | |||||
pidfile_template = _getopt(p.options, ['--pidfile', '-p']) | |||||
pid = None | |||||
pidfile = node.expander(pidfile_template) | |||||
try: | |||||
pid = Pidfile(pidfile).read_pid() | |||||
except ValueError: | |||||
pass | |||||
if pid: | |||||
nodes.append((node.name, tuple(node.argv), pid)) | |||||
else: | |||||
self.note('> {0.name}: {1}'.format(node, self.DOWN)) | |||||
if callback: | |||||
callback(node.name, node.argv, pid) | |||||
return nodes | |||||
def kill(self, argv, cmd): | |||||
self.splash() | |||||
p = NamespacedOptionParser(argv) | |||||
for nodename, _, pid in self.getpids(p, cmd): | |||||
self.note('Killing node {0} ({1})'.format(nodename, pid)) | |||||
self.signal_node(nodename, pid, signal.SIGKILL) | |||||
def stop(self, argv, cmd, retry=None, callback=None): | |||||
self.splash() | |||||
p = NamespacedOptionParser(argv) | |||||
return self._stop_nodes(p, cmd, retry=retry, callback=callback) | |||||
def _stop_nodes(self, p, cmd, retry=None, callback=None): | |||||
restargs = p.args[len(p.values):] | |||||
self.shutdown_nodes(self.getpids(p, cmd, callback=callback), | |||||
sig=findsig(restargs), | |||||
retry=retry, | |||||
callback=callback) | |||||
def restart(self, argv, cmd): | |||||
self.splash() | |||||
p = NamespacedOptionParser(argv) | |||||
self.with_detacher_default_options(p) | |||||
retvals = [] | |||||
def on_node_shutdown(nodename, argv, pid): | |||||
self.note(self.colored.blue( | |||||
'> Restarting node {0}: '.format(nodename)), newline=False) | |||||
retval = self.waitexec(argv, path=p.options['--executable']) | |||||
self.note(retval and self.FAILED or self.OK) | |||||
retvals.append(retval) | |||||
self._stop_nodes(p, cmd, retry=2, callback=on_node_shutdown) | |||||
self.retval = int(any(retvals)) | |||||
def stopwait(self, argv, cmd): | |||||
self.splash() | |||||
p = NamespacedOptionParser(argv) | |||||
self.with_detacher_default_options(p) | |||||
return self._stop_nodes(p, cmd, retry=2) | |||||
stop_verify = stopwait # compat | |||||
def expand(self, argv, cmd=None): | |||||
template = argv[0] | |||||
p = NamespacedOptionParser(argv[1:]) | |||||
for node in multi_args(p, cmd): | |||||
self.say(node.expander(template)) | |||||
def help(self, argv, cmd=None): | |||||
self.say(__doc__) | |||||
def usage(self): | |||||
self.splash() | |||||
self.say(USAGE.format(prog_name=self.prog_name)) | |||||
def splash(self): | |||||
if not self.nosplash: | |||||
c = self.colored | |||||
self.note(c.cyan('celery multi v{0}'.format(VERSION_BANNER))) | |||||
def waitexec(self, argv, path=sys.executable): | |||||
args = ' '.join([path] + list(argv)) | |||||
argstr = shlex.split(from_utf8(args), posix=not IS_WINDOWS) | |||||
pipe = Popen(argstr, env=self.env) | |||||
self.info(' {0}'.format(' '.join(argstr))) | |||||
retcode = pipe.wait() | |||||
if retcode < 0: | |||||
self.note('* Child was terminated by signal {0}'.format(-retcode)) | |||||
return -retcode | |||||
elif retcode > 0: | |||||
self.note('* Child terminated with errorcode {0}'.format(retcode)) | |||||
return retcode | |||||
def error(self, msg=None): | |||||
if msg: | |||||
self.carp(msg) | |||||
self.usage() | |||||
self.retcode = 1 | |||||
return 1 | |||||
def info(self, msg, newline=True): | |||||
if self.verbose: | |||||
self.note(msg, newline=newline) | |||||
def note(self, msg, newline=True): | |||||
if not self.quiet: | |||||
self.say(str(msg), newline=newline) | |||||
@cached_property | |||||
def colored(self): | |||||
return term.colored(enabled=not self.no_color) | |||||
@cached_property | |||||
def OK(self): | |||||
return str(self.colored.green('OK')) | |||||
@cached_property | |||||
def FAILED(self): | |||||
return str(self.colored.red('FAILED')) | |||||
@cached_property | |||||
def DOWN(self): | |||||
return str(self.colored.magenta('DOWN')) | |||||
def multi_args(p, cmd='celery worker', append='', prefix='', suffix=''): | |||||
names = p.values | |||||
options = dict(p.options) | |||||
passthrough = p.passthrough | |||||
ranges = len(names) == 1 | |||||
if ranges: | |||||
try: | |||||
noderange = int(names[0]) | |||||
except ValueError: | |||||
pass | |||||
else: | |||||
names = [str(n) for n in range(1, noderange + 1)] | |||||
prefix = 'celery' | |||||
cmd = options.pop('--cmd', cmd) | |||||
append = options.pop('--append', append) | |||||
hostname = options.pop('--hostname', | |||||
options.pop('-n', socket.gethostname())) | |||||
prefix = options.pop('--prefix', prefix) or '' | |||||
suffix = options.pop('--suffix', suffix) or hostname | |||||
if suffix in ('""', "''"): | |||||
suffix = '' | |||||
for ns_name, ns_opts in list(items(p.namespaces)): | |||||
if ',' in ns_name or (ranges and '-' in ns_name): | |||||
for subns in parse_ns_range(ns_name, ranges): | |||||
p.namespaces[subns].update(ns_opts) | |||||
p.namespaces.pop(ns_name) | |||||
# Numbers in args always refers to the index in the list of names. | |||||
# (e.g. `start foo bar baz -c:1` where 1 is foo, 2 is bar, and so on). | |||||
for ns_name, ns_opts in list(items(p.namespaces)): | |||||
if ns_name.isdigit(): | |||||
ns_index = int(ns_name) - 1 | |||||
if ns_index < 0: | |||||
raise KeyError('Indexes start at 1 got: %r' % (ns_name, )) | |||||
try: | |||||
p.namespaces[names[ns_index]].update(ns_opts) | |||||
except IndexError: | |||||
raise KeyError('No node at index %r' % (ns_name, )) | |||||
for name in names: | |||||
this_suffix = suffix | |||||
if '@' in name: | |||||
this_name = options['-n'] = name | |||||
nodename, this_suffix = nodesplit(name) | |||||
name = nodename | |||||
else: | |||||
nodename = '%s%s' % (prefix, name) | |||||
this_name = options['-n'] = '%s@%s' % (nodename, this_suffix) | |||||
expand = abbreviations({'%h': this_name, | |||||
'%n': name, | |||||
'%N': nodename, | |||||
'%d': this_suffix}) | |||||
argv = ([expand(cmd)] + | |||||
[format_opt(opt, expand(value)) | |||||
for opt, value in items(p.optmerge(name, options))] + | |||||
[passthrough]) | |||||
if append: | |||||
argv.append(expand(append)) | |||||
yield multi_args_t(this_name, argv, expand, name) | |||||
class NamespacedOptionParser(object): | |||||
def __init__(self, args): | |||||
self.args = args | |||||
self.options = OrderedDict() | |||||
self.values = [] | |||||
self.passthrough = '' | |||||
self.namespaces = defaultdict(lambda: OrderedDict()) | |||||
self.parse() | |||||
def parse(self): | |||||
rargs = list(self.args) | |||||
pos = 0 | |||||
while pos < len(rargs): | |||||
arg = rargs[pos] | |||||
if arg == '--': | |||||
self.passthrough = ' '.join(rargs[pos:]) | |||||
break | |||||
elif arg[0] == '-': | |||||
if arg[1] == '-': | |||||
self.process_long_opt(arg[2:]) | |||||
else: | |||||
value = None | |||||
if len(rargs) > pos + 1 and rargs[pos + 1][0] != '-': | |||||
value = rargs[pos + 1] | |||||
pos += 1 | |||||
self.process_short_opt(arg[1:], value) | |||||
else: | |||||
self.values.append(arg) | |||||
pos += 1 | |||||
def process_long_opt(self, arg, value=None): | |||||
if '=' in arg: | |||||
arg, value = arg.split('=', 1) | |||||
self.add_option(arg, value, short=False) | |||||
def process_short_opt(self, arg, value=None): | |||||
self.add_option(arg, value, short=True) | |||||
def optmerge(self, ns, defaults=None): | |||||
if defaults is None: | |||||
defaults = self.options | |||||
return OrderedDict(defaults, **self.namespaces[ns]) | |||||
def add_option(self, name, value, short=False, ns=None): | |||||
prefix = short and '-' or '--' | |||||
dest = self.options | |||||
if ':' in name: | |||||
name, ns = name.split(':') | |||||
dest = self.namespaces[ns] | |||||
dest[prefix + name] = value | |||||
def quote(v): | |||||
return "\\'".join("'" + p + "'" for p in v.split("'")) | |||||
def format_opt(opt, value): | |||||
if not value: | |||||
return opt | |||||
if opt.startswith('--'): | |||||
return '{0}={1}'.format(opt, value) | |||||
return '{0} {1}'.format(opt, value) | |||||
def parse_ns_range(ns, ranges=False): | |||||
ret = [] | |||||
for space in ',' in ns and ns.split(',') or [ns]: | |||||
if ranges and '-' in space: | |||||
start, stop = space.split('-') | |||||
ret.extend( | |||||
str(n) for n in range(int(start), int(stop) + 1) | |||||
) | |||||
else: | |||||
ret.append(space) | |||||
return ret | |||||
def abbreviations(mapping): | |||||
def expand(S): | |||||
ret = S | |||||
if S is not None: | |||||
for short_opt, long_opt in items(mapping): | |||||
ret = ret.replace(short_opt, long_opt) | |||||
return ret | |||||
return expand | |||||
def findsig(args, default=signal.SIGTERM): | |||||
for arg in reversed(args): | |||||
if len(arg) == 2 and arg[0] == '-': | |||||
try: | |||||
return int(arg[1]) | |||||
except ValueError: | |||||
pass | |||||
if arg[0] == '-': | |||||
maybe_sig = 'SIG' + arg[1:] | |||||
if maybe_sig in SIGNAMES: | |||||
return getattr(signal, maybe_sig) | |||||
return default | |||||
def _getopt(d, alt): | |||||
for opt in alt: | |||||
try: | |||||
return d[opt] | |||||
except KeyError: | |||||
pass | |||||
raise KeyError(alt[0]) | |||||
def _setdefaultopt(d, alt, value): | |||||
for opt in alt[1:]: | |||||
try: | |||||
return d[opt] | |||||
except KeyError: | |||||
pass | |||||
return d.setdefault(alt[0], value) | |||||
if __name__ == '__main__': # pragma: no cover | |||||
main() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
The :program:`celery worker` command (previously known as ``celeryd``) | |||||
.. program:: celery worker | |||||
.. seealso:: | |||||
See :ref:`preload-options`. | |||||
.. cmdoption:: -c, --concurrency | |||||
Number of child processes processing the queue. The default | |||||
is the number of CPUs available on your system. | |||||
.. cmdoption:: -P, --pool | |||||
Pool implementation: | |||||
prefork (default), eventlet, gevent, solo or threads. | |||||
.. cmdoption:: -f, --logfile | |||||
Path to log file. If no logfile is specified, `stderr` is used. | |||||
.. cmdoption:: -l, --loglevel | |||||
Logging level, choose between `DEBUG`, `INFO`, `WARNING`, | |||||
`ERROR`, `CRITICAL`, or `FATAL`. | |||||
.. cmdoption:: -n, --hostname | |||||
Set custom hostname, e.g. 'w1.%h'. Expands: %h (hostname), | |||||
%n (name) and %d, (domain). | |||||
.. cmdoption:: -B, --beat | |||||
Also run the `celery beat` periodic task scheduler. Please note that | |||||
there must only be one instance of this service. | |||||
.. cmdoption:: -Q, --queues | |||||
List of queues to enable for this worker, separated by comma. | |||||
By default all configured queues are enabled. | |||||
Example: `-Q video,image` | |||||
.. cmdoption:: -I, --include | |||||
Comma separated list of additional modules to import. | |||||
Example: -I foo.tasks,bar.tasks | |||||
.. cmdoption:: -s, --schedule | |||||
Path to the schedule database if running with the `-B` option. | |||||
Defaults to `celerybeat-schedule`. The extension ".db" may be | |||||
appended to the filename. | |||||
.. cmdoption:: -O | |||||
Apply optimization profile. Supported: default, fair | |||||
.. cmdoption:: --scheduler | |||||
Scheduler class to use. Default is celery.beat.PersistentScheduler | |||||
.. cmdoption:: -S, --statedb | |||||
Path to the state database. The extension '.db' may | |||||
be appended to the filename. Default: {default} | |||||
.. cmdoption:: -E, --events | |||||
Send events that can be captured by monitors like :program:`celery events`, | |||||
`celerymon`, and others. | |||||
.. cmdoption:: --without-gossip | |||||
Do not subscribe to other workers events. | |||||
.. cmdoption:: --without-mingle | |||||
Do not synchronize with other workers at startup. | |||||
.. cmdoption:: --without-heartbeat | |||||
Do not send event heartbeats. | |||||
.. cmdoption:: --heartbeat-interval | |||||
Interval in seconds at which to send worker heartbeat | |||||
.. cmdoption:: --purge | |||||
Purges all waiting tasks before the daemon is started. | |||||
**WARNING**: This is unrecoverable, and the tasks will be | |||||
deleted from the messaging server. | |||||
.. cmdoption:: --time-limit | |||||
Enables a hard time limit (in seconds int/float) for tasks. | |||||
.. cmdoption:: --soft-time-limit | |||||
Enables a soft time limit (in seconds int/float) for tasks. | |||||
.. cmdoption:: --maxtasksperchild | |||||
Maximum number of tasks a pool worker can execute before it's | |||||
terminated and replaced by a new worker. | |||||
.. cmdoption:: --pidfile | |||||
Optional file used to store the workers pid. | |||||
The worker will not start if this file already exists | |||||
and the pid is still alive. | |||||
.. cmdoption:: --autoscale | |||||
Enable autoscaling by providing | |||||
max_concurrency, min_concurrency. Example:: | |||||
--autoscale=10,3 | |||||
(always keep 3 processes, but grow to 10 if necessary) | |||||
.. cmdoption:: --autoreload | |||||
Enable autoreloading. | |||||
.. cmdoption:: --no-execv | |||||
Don't do execv after multiprocessing child fork. | |||||
""" | |||||
from __future__ import absolute_import, unicode_literals | |||||
import sys | |||||
from celery import concurrency | |||||
from celery.bin.base import Command, Option, daemon_options | |||||
from celery.bin.celeryd_detach import detached_celeryd | |||||
from celery.five import string_t | |||||
from celery.platforms import maybe_drop_privileges | |||||
from celery.utils import default_nodename | |||||
from celery.utils.log import LOG_LEVELS, mlevel | |||||
__all__ = ['worker', 'main'] | |||||
__MODULE_DOC__ = __doc__ | |||||
class worker(Command): | |||||
"""Start worker instance. | |||||
Examples:: | |||||
celery worker --app=proj -l info | |||||
celery worker -A proj -l info -Q hipri,lopri | |||||
celery worker -A proj --concurrency=4 | |||||
celery worker -A proj --concurrency=1000 -P eventlet | |||||
celery worker --autoscale=10,0 | |||||
""" | |||||
doc = __MODULE_DOC__ # parse help from this too | |||||
namespace = 'celeryd' | |||||
enable_config_from_cmdline = True | |||||
supports_args = False | |||||
def run_from_argv(self, prog_name, argv=None, command=None): | |||||
command = sys.argv[0] if command is None else command | |||||
argv = sys.argv[1:] if argv is None else argv | |||||
# parse options before detaching so errors can be handled. | |||||
options, args = self.prepare_args( | |||||
*self.parse_options(prog_name, argv, command)) | |||||
self.maybe_detach([command] + argv) | |||||
return self(*args, **options) | |||||
def maybe_detach(self, argv, dopts=['-D', '--detach']): | |||||
if any(arg in argv for arg in dopts): | |||||
argv = [v for v in argv if v not in dopts] | |||||
# will never return | |||||
detached_celeryd(self.app).execute_from_commandline(argv) | |||||
raise SystemExit(0) | |||||
def run(self, hostname=None, pool_cls=None, app=None, uid=None, gid=None, | |||||
loglevel=None, logfile=None, pidfile=None, state_db=None, | |||||
**kwargs): | |||||
maybe_drop_privileges(uid=uid, gid=gid) | |||||
# Pools like eventlet/gevent needs to patch libs as early | |||||
# as possible. | |||||
pool_cls = (concurrency.get_implementation(pool_cls) or | |||||
self.app.conf.CELERYD_POOL) | |||||
if self.app.IS_WINDOWS and kwargs.get('beat'): | |||||
self.die('-B option does not work on Windows. ' | |||||
'Please run celery beat as a separate service.') | |||||
hostname = self.host_format(default_nodename(hostname)) | |||||
if loglevel: | |||||
try: | |||||
loglevel = mlevel(loglevel) | |||||
except KeyError: # pragma: no cover | |||||
self.die('Unknown level {0!r}. Please use one of {1}.'.format( | |||||
loglevel, '|'.join( | |||||
l for l in LOG_LEVELS if isinstance(l, string_t)))) | |||||
return self.app.Worker( | |||||
hostname=hostname, pool_cls=pool_cls, loglevel=loglevel, | |||||
logfile=logfile, # node format handled by celery.app.log.setup | |||||
pidfile=self.node_format(pidfile, hostname), | |||||
state_db=self.node_format(state_db, hostname), **kwargs | |||||
).start() | |||||
def with_pool_option(self, argv): | |||||
# this command support custom pools | |||||
# that may have to be loaded as early as possible. | |||||
return (['-P'], ['--pool']) | |||||
def get_options(self): | |||||
conf = self.app.conf | |||||
return ( | |||||
Option('-c', '--concurrency', | |||||
default=conf.CELERYD_CONCURRENCY, type='int'), | |||||
Option('-P', '--pool', default=conf.CELERYD_POOL, dest='pool_cls'), | |||||
Option('--purge', '--discard', default=False, action='store_true'), | |||||
Option('-l', '--loglevel', default=conf.CELERYD_LOG_LEVEL), | |||||
Option('-n', '--hostname'), | |||||
Option('-B', '--beat', action='store_true'), | |||||
Option('-s', '--schedule', dest='schedule_filename', | |||||
default=conf.CELERYBEAT_SCHEDULE_FILENAME), | |||||
Option('--scheduler', dest='scheduler_cls'), | |||||
Option('-S', '--statedb', | |||||
default=conf.CELERYD_STATE_DB, dest='state_db'), | |||||
Option('-E', '--events', default=conf.CELERY_SEND_EVENTS, | |||||
action='store_true', dest='send_events'), | |||||
Option('--time-limit', type='float', dest='task_time_limit', | |||||
default=conf.CELERYD_TASK_TIME_LIMIT), | |||||
Option('--soft-time-limit', dest='task_soft_time_limit', | |||||
default=conf.CELERYD_TASK_SOFT_TIME_LIMIT, type='float'), | |||||
Option('--maxtasksperchild', dest='max_tasks_per_child', | |||||
default=conf.CELERYD_MAX_TASKS_PER_CHILD, type='int'), | |||||
Option('--queues', '-Q', default=[]), | |||||
Option('--exclude-queues', '-X', default=[]), | |||||
Option('--include', '-I', default=[]), | |||||
Option('--autoscale'), | |||||
Option('--autoreload', action='store_true'), | |||||
Option('--no-execv', action='store_true', default=False), | |||||
Option('--without-gossip', action='store_true', default=False), | |||||
Option('--without-mingle', action='store_true', default=False), | |||||
Option('--without-heartbeat', action='store_true', default=False), | |||||
Option('--heartbeat-interval', type='int'), | |||||
Option('-O', dest='optimization'), | |||||
Option('-D', '--detach', action='store_true'), | |||||
) + daemon_options() + tuple(self.app.user_options['worker']) | |||||
def main(app=None): | |||||
# Fix for setuptools generated scripts, so that it will | |||||
# work with multiprocessing fork emulation. | |||||
# (see multiprocessing.forking.get_preparation_data()) | |||||
if __name__ != '__main__': # pragma: no cover | |||||
sys.modules['__main__'] = sys.modules[__name__] | |||||
from billiard import freeze_support | |||||
freeze_support() | |||||
worker(app=app).execute_from_commandline() | |||||
if __name__ == '__main__': # pragma: no cover | |||||
main() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.bootsteps | |||||
~~~~~~~~~~~~~~~~ | |||||
A directed acyclic graph of reusable components. | |||||
""" | |||||
from __future__ import absolute_import, unicode_literals | |||||
from collections import deque | |||||
from threading import Event | |||||
from kombu.common import ignore_errors | |||||
from kombu.utils import symbol_by_name | |||||
from .datastructures import DependencyGraph, GraphFormatter | |||||
from .five import values, with_metaclass | |||||
from .utils.imports import instantiate, qualname | |||||
from .utils.log import get_logger | |||||
try: | |||||
from greenlet import GreenletExit | |||||
IGNORE_ERRORS = (GreenletExit, ) | |||||
except ImportError: # pragma: no cover | |||||
IGNORE_ERRORS = () | |||||
__all__ = ['Blueprint', 'Step', 'StartStopStep', 'ConsumerStep'] | |||||
#: States | |||||
RUN = 0x1 | |||||
CLOSE = 0x2 | |||||
TERMINATE = 0x3 | |||||
logger = get_logger(__name__) | |||||
debug = logger.debug | |||||
def _pre(ns, fmt): | |||||
return '| {0}: {1}'.format(ns.alias, fmt) | |||||
def _label(s): | |||||
return s.name.rsplit('.', 1)[-1] | |||||
class StepFormatter(GraphFormatter): | |||||
"""Graph formatter for :class:`Blueprint`.""" | |||||
blueprint_prefix = '⧉' | |||||
conditional_prefix = '∘' | |||||
blueprint_scheme = { | |||||
'shape': 'parallelogram', | |||||
'color': 'slategray4', | |||||
'fillcolor': 'slategray3', | |||||
} | |||||
def label(self, step): | |||||
return step and '{0}{1}'.format( | |||||
self._get_prefix(step), | |||||
(step.label or _label(step)).encode('utf-8', 'ignore'), | |||||
) | |||||
def _get_prefix(self, step): | |||||
if step.last: | |||||
return self.blueprint_prefix | |||||
if step.conditional: | |||||
return self.conditional_prefix | |||||
return '' | |||||
def node(self, obj, **attrs): | |||||
scheme = self.blueprint_scheme if obj.last else self.node_scheme | |||||
return self.draw_node(obj, scheme, attrs) | |||||
def edge(self, a, b, **attrs): | |||||
if a.last: | |||||
attrs.update(arrowhead='none', color='darkseagreen3') | |||||
return self.draw_edge(a, b, self.edge_scheme, attrs) | |||||
class Blueprint(object): | |||||
"""Blueprint containing bootsteps that can be applied to objects. | |||||
:keyword steps: List of steps. | |||||
:keyword name: Set explicit name for this blueprint. | |||||
:keyword app: Set the Celery app for this blueprint. | |||||
:keyword on_start: Optional callback applied after blueprint start. | |||||
:keyword on_close: Optional callback applied before blueprint close. | |||||
:keyword on_stopped: Optional callback applied after blueprint stopped. | |||||
""" | |||||
GraphFormatter = StepFormatter | |||||
name = None | |||||
state = None | |||||
started = 0 | |||||
default_steps = set() | |||||
state_to_name = { | |||||
0: 'initializing', | |||||
RUN: 'running', | |||||
CLOSE: 'closing', | |||||
TERMINATE: 'terminating', | |||||
} | |||||
def __init__(self, steps=None, name=None, app=None, | |||||
on_start=None, on_close=None, on_stopped=None): | |||||
self.app = app | |||||
self.name = name or self.name or qualname(type(self)) | |||||
self.types = set(steps or []) | set(self.default_steps) | |||||
self.on_start = on_start | |||||
self.on_close = on_close | |||||
self.on_stopped = on_stopped | |||||
self.shutdown_complete = Event() | |||||
self.steps = {} | |||||
def start(self, parent): | |||||
self.state = RUN | |||||
if self.on_start: | |||||
self.on_start() | |||||
for i, step in enumerate(s for s in parent.steps if s is not None): | |||||
self._debug('Starting %s', step.alias) | |||||
self.started = i + 1 | |||||
step.start(parent) | |||||
debug('^-- substep ok') | |||||
def human_state(self): | |||||
return self.state_to_name[self.state or 0] | |||||
def info(self, parent): | |||||
info = {} | |||||
for step in parent.steps: | |||||
info.update(step.info(parent) or {}) | |||||
return info | |||||
def close(self, parent): | |||||
if self.on_close: | |||||
self.on_close() | |||||
self.send_all(parent, 'close', 'closing', reverse=False) | |||||
def restart(self, parent, method='stop', | |||||
description='restarting', propagate=False): | |||||
self.send_all(parent, method, description, propagate=propagate) | |||||
def send_all(self, parent, method, | |||||
description=None, reverse=True, propagate=True, args=()): | |||||
description = description or method.replace('_', ' ') | |||||
steps = reversed(parent.steps) if reverse else parent.steps | |||||
for step in steps: | |||||
if step: | |||||
fun = getattr(step, method, None) | |||||
if fun is not None: | |||||
self._debug('%s %s...', | |||||
description.capitalize(), step.alias) | |||||
try: | |||||
fun(parent, *args) | |||||
except Exception as exc: | |||||
if propagate: | |||||
raise | |||||
logger.error( | |||||
'Error on %s %s: %r', | |||||
description, step.alias, exc, exc_info=1, | |||||
) | |||||
def stop(self, parent, close=True, terminate=False): | |||||
what = 'terminating' if terminate else 'stopping' | |||||
if self.state in (CLOSE, TERMINATE): | |||||
return | |||||
if self.state != RUN or self.started != len(parent.steps): | |||||
# Not fully started, can safely exit. | |||||
self.state = TERMINATE | |||||
self.shutdown_complete.set() | |||||
return | |||||
self.close(parent) | |||||
self.state = CLOSE | |||||
self.restart( | |||||
parent, 'terminate' if terminate else 'stop', | |||||
description=what, propagate=False, | |||||
) | |||||
if self.on_stopped: | |||||
self.on_stopped() | |||||
self.state = TERMINATE | |||||
self.shutdown_complete.set() | |||||
def join(self, timeout=None): | |||||
try: | |||||
# Will only get here if running green, | |||||
# makes sure all greenthreads have exited. | |||||
self.shutdown_complete.wait(timeout=timeout) | |||||
except IGNORE_ERRORS: | |||||
pass | |||||
def apply(self, parent, **kwargs): | |||||
"""Apply the steps in this blueprint to an object. | |||||
This will apply the ``__init__`` and ``include`` methods | |||||
of each step, with the object as argument:: | |||||
step = Step(obj) | |||||
... | |||||
step.include(obj) | |||||
For :class:`StartStopStep` the services created | |||||
will also be added to the objects ``steps`` attribute. | |||||
""" | |||||
self._debug('Preparing bootsteps.') | |||||
order = self.order = [] | |||||
steps = self.steps = self.claim_steps() | |||||
self._debug('Building graph...') | |||||
for S in self._finalize_steps(steps): | |||||
step = S(parent, **kwargs) | |||||
steps[step.name] = step | |||||
order.append(step) | |||||
self._debug('New boot order: {%s}', | |||||
', '.join(s.alias for s in self.order)) | |||||
for step in order: | |||||
step.include(parent) | |||||
return self | |||||
def connect_with(self, other): | |||||
self.graph.adjacent.update(other.graph.adjacent) | |||||
self.graph.add_edge(type(other.order[0]), type(self.order[-1])) | |||||
def __getitem__(self, name): | |||||
return self.steps[name] | |||||
def _find_last(self): | |||||
return next((C for C in values(self.steps) if C.last), None) | |||||
def _firstpass(self, steps): | |||||
for step in values(steps): | |||||
step.requires = [symbol_by_name(dep) for dep in step.requires] | |||||
stream = deque(step.requires for step in values(steps)) | |||||
while stream: | |||||
for node in stream.popleft(): | |||||
node = symbol_by_name(node) | |||||
if node.name not in self.steps: | |||||
steps[node.name] = node | |||||
stream.append(node.requires) | |||||
def _finalize_steps(self, steps): | |||||
last = self._find_last() | |||||
self._firstpass(steps) | |||||
it = ((C, C.requires) for C in values(steps)) | |||||
G = self.graph = DependencyGraph( | |||||
it, formatter=self.GraphFormatter(root=last), | |||||
) | |||||
if last: | |||||
for obj in G: | |||||
if obj != last: | |||||
G.add_edge(last, obj) | |||||
try: | |||||
return G.topsort() | |||||
except KeyError as exc: | |||||
raise KeyError('unknown bootstep: %s' % exc) | |||||
def claim_steps(self): | |||||
return dict(self.load_step(step) for step in self._all_steps()) | |||||
def _all_steps(self): | |||||
return self.types | self.app.steps[self.name.lower()] | |||||
def load_step(self, step): | |||||
step = symbol_by_name(step) | |||||
return step.name, step | |||||
def _debug(self, msg, *args): | |||||
return debug(_pre(self, msg), *args) | |||||
@property | |||||
def alias(self): | |||||
return _label(self) | |||||
class StepType(type): | |||||
"""Metaclass for steps.""" | |||||
def __new__(cls, name, bases, attrs): | |||||
module = attrs.get('__module__') | |||||
qname = '{0}.{1}'.format(module, name) if module else name | |||||
attrs.update( | |||||
__qualname__=qname, | |||||
name=attrs.get('name') or qname, | |||||
) | |||||
return super(StepType, cls).__new__(cls, name, bases, attrs) | |||||
def __str__(self): | |||||
return self.name | |||||
def __repr__(self): | |||||
return 'step:{0.name}{{{0.requires!r}}}'.format(self) | |||||
@with_metaclass(StepType) | |||||
class Step(object): | |||||
"""A Bootstep. | |||||
The :meth:`__init__` method is called when the step | |||||
is bound to a parent object, and can as such be used | |||||
to initialize attributes in the parent object at | |||||
parent instantiation-time. | |||||
""" | |||||
#: Optional step name, will use qualname if not specified. | |||||
name = None | |||||
#: Optional short name used for graph outputs and in logs. | |||||
label = None | |||||
#: Set this to true if the step is enabled based on some condition. | |||||
conditional = False | |||||
#: List of other steps that that must be started before this step. | |||||
#: Note that all dependencies must be in the same blueprint. | |||||
requires = () | |||||
#: This flag is reserved for the workers Consumer, | |||||
#: since it is required to always be started last. | |||||
#: There can only be one object marked last | |||||
#: in every blueprint. | |||||
last = False | |||||
#: This provides the default for :meth:`include_if`. | |||||
enabled = True | |||||
def __init__(self, parent, **kwargs): | |||||
pass | |||||
def include_if(self, parent): | |||||
"""An optional predicate that decides whether this | |||||
step should be created.""" | |||||
return self.enabled | |||||
def instantiate(self, name, *args, **kwargs): | |||||
return instantiate(name, *args, **kwargs) | |||||
def _should_include(self, parent): | |||||
if self.include_if(parent): | |||||
return True, self.create(parent) | |||||
return False, None | |||||
def include(self, parent): | |||||
return self._should_include(parent)[0] | |||||
def create(self, parent): | |||||
"""Create the step.""" | |||||
pass | |||||
def __repr__(self): | |||||
return '<step: {0.alias}>'.format(self) | |||||
@property | |||||
def alias(self): | |||||
return self.label or _label(self) | |||||
def info(self, obj): | |||||
pass | |||||
class StartStopStep(Step): | |||||
#: Optional obj created by the :meth:`create` method. | |||||
#: This is used by :class:`StartStopStep` to keep the | |||||
#: original service object. | |||||
obj = None | |||||
def start(self, parent): | |||||
if self.obj: | |||||
return self.obj.start() | |||||
def stop(self, parent): | |||||
if self.obj: | |||||
return self.obj.stop() | |||||
def close(self, parent): | |||||
pass | |||||
def terminate(self, parent): | |||||
if self.obj: | |||||
return getattr(self.obj, 'terminate', self.obj.stop)() | |||||
def include(self, parent): | |||||
inc, ret = self._should_include(parent) | |||||
if inc: | |||||
self.obj = ret | |||||
parent.steps.append(self) | |||||
return inc | |||||
class ConsumerStep(StartStopStep): | |||||
requires = ('celery.worker.consumer:Connection', ) | |||||
consumers = None | |||||
def get_consumers(self, channel): | |||||
raise NotImplementedError('missing get_consumers') | |||||
def start(self, c): | |||||
channel = c.connection.channel() | |||||
self.consumers = self.get_consumers(channel) | |||||
for consumer in self.consumers or []: | |||||
consumer.consume() | |||||
def stop(self, c): | |||||
self._close(c, True) | |||||
def shutdown(self, c): | |||||
self._close(c, False) | |||||
def _close(self, c, cancel_consumers=True): | |||||
channels = set() | |||||
for consumer in self.consumers or []: | |||||
if cancel_consumers: | |||||
ignore_errors(c.connection, consumer.cancel) | |||||
if consumer.channel: | |||||
channels.add(consumer.channel) | |||||
for channel in channels: | |||||
ignore_errors(c.connection, channel.close) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.canvas | |||||
~~~~~~~~~~~~~ | |||||
Composing task workflows. | |||||
Documentation for some of these types are in :mod:`celery`. | |||||
You should import these from :mod:`celery` and not this module. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from collections import MutableSequence | |||||
from copy import deepcopy | |||||
from functools import partial as _partial, reduce | |||||
from operator import itemgetter | |||||
from itertools import chain as _chain | |||||
from kombu.utils import cached_property, fxrange, kwdict, reprcall, uuid | |||||
from celery._state import current_app | |||||
from celery.utils.functional import ( | |||||
maybe_list, is_list, regen, | |||||
chunks as _chunks, | |||||
) | |||||
from celery.utils.text import truncate | |||||
__all__ = ['Signature', 'chain', 'xmap', 'xstarmap', 'chunks', | |||||
'group', 'chord', 'signature', 'maybe_signature'] | |||||
class _getitem_property(object): | |||||
"""Attribute -> dict key descriptor. | |||||
The target object must support ``__getitem__``, | |||||
and optionally ``__setitem__``. | |||||
Example: | |||||
>>> from collections import defaultdict | |||||
>>> class Me(dict): | |||||
... deep = defaultdict(dict) | |||||
... | |||||
... foo = _getitem_property('foo') | |||||
... deep_thing = _getitem_property('deep.thing') | |||||
>>> me = Me() | |||||
>>> me.foo | |||||
None | |||||
>>> me.foo = 10 | |||||
>>> me.foo | |||||
10 | |||||
>>> me['foo'] | |||||
10 | |||||
>>> me.deep_thing = 42 | |||||
>>> me.deep_thing | |||||
42 | |||||
>>> me.deep | |||||
defaultdict(<type 'dict'>, {'thing': 42}) | |||||
""" | |||||
def __init__(self, keypath): | |||||
path, _, self.key = keypath.rpartition('.') | |||||
self.path = path.split('.') if path else None | |||||
def _path(self, obj): | |||||
return (reduce(lambda d, k: d[k], [obj] + self.path) if self.path | |||||
else obj) | |||||
def __get__(self, obj, type=None): | |||||
if obj is None: | |||||
return type | |||||
return self._path(obj).get(self.key) | |||||
def __set__(self, obj, value): | |||||
self._path(obj)[self.key] = value | |||||
def maybe_unroll_group(g): | |||||
"""Unroll group with only one member.""" | |||||
# Issue #1656 | |||||
try: | |||||
size = len(g.tasks) | |||||
except TypeError: | |||||
try: | |||||
size = g.tasks.__length_hint__() | |||||
except (AttributeError, TypeError): | |||||
pass | |||||
else: | |||||
return list(g.tasks)[0] if size == 1 else g | |||||
else: | |||||
return g.tasks[0] if size == 1 else g | |||||
def _upgrade(fields, sig): | |||||
"""Used by custom signatures in .from_dict, to keep common fields.""" | |||||
sig.update(chord_size=fields.get('chord_size')) | |||||
return sig | |||||
class Signature(dict): | |||||
"""Class that wraps the arguments and execution options | |||||
for a single task invocation. | |||||
Used as the parts in a :class:`group` and other constructs, | |||||
or to pass tasks around as callbacks while being compatible | |||||
with serializers with a strict type subset. | |||||
:param task: Either a task class/instance, or the name of a task. | |||||
:keyword args: Positional arguments to apply. | |||||
:keyword kwargs: Keyword arguments to apply. | |||||
:keyword options: Additional options to :meth:`Task.apply_async`. | |||||
Note that if the first argument is a :class:`dict`, the other | |||||
arguments will be ignored and the values in the dict will be used | |||||
instead. | |||||
>>> s = signature('tasks.add', args=(2, 2)) | |||||
>>> signature(s) | |||||
{'task': 'tasks.add', args=(2, 2), kwargs={}, options={}} | |||||
""" | |||||
TYPES = {} | |||||
_app = _type = None | |||||
@classmethod | |||||
def register_type(cls, subclass, name=None): | |||||
cls.TYPES[name or subclass.__name__] = subclass | |||||
return subclass | |||||
@classmethod | |||||
def from_dict(self, d, app=None): | |||||
typ = d.get('subtask_type') | |||||
if typ: | |||||
return self.TYPES[typ].from_dict(kwdict(d), app=app) | |||||
return Signature(d, app=app) | |||||
def __init__(self, task=None, args=None, kwargs=None, options=None, | |||||
type=None, subtask_type=None, immutable=False, | |||||
app=None, **ex): | |||||
self._app = app | |||||
init = dict.__init__ | |||||
if isinstance(task, dict): | |||||
return init(self, task) # works like dict(d) | |||||
# Also supports using task class/instance instead of string name. | |||||
try: | |||||
task_name = task.name | |||||
except AttributeError: | |||||
task_name = task | |||||
else: | |||||
self._type = task | |||||
init(self, | |||||
task=task_name, args=tuple(args or ()), | |||||
kwargs=kwargs or {}, | |||||
options=dict(options or {}, **ex), | |||||
subtask_type=subtask_type, | |||||
immutable=immutable, | |||||
chord_size=None) | |||||
def __call__(self, *partial_args, **partial_kwargs): | |||||
args, kwargs, _ = self._merge(partial_args, partial_kwargs, None) | |||||
return self.type(*args, **kwargs) | |||||
def delay(self, *partial_args, **partial_kwargs): | |||||
return self.apply_async(partial_args, partial_kwargs) | |||||
def apply(self, args=(), kwargs={}, **options): | |||||
"""Apply this task locally.""" | |||||
# For callbacks: extra args are prepended to the stored args. | |||||
args, kwargs, options = self._merge(args, kwargs, options) | |||||
return self.type.apply(args, kwargs, **options) | |||||
def _merge(self, args=(), kwargs={}, options={}): | |||||
if self.immutable: | |||||
return (self.args, self.kwargs, | |||||
dict(self.options, **options) if options else self.options) | |||||
return (tuple(args) + tuple(self.args) if args else self.args, | |||||
dict(self.kwargs, **kwargs) if kwargs else self.kwargs, | |||||
dict(self.options, **options) if options else self.options) | |||||
def clone(self, args=(), kwargs={}, app=None, **opts): | |||||
# need to deepcopy options so origins links etc. is not modified. | |||||
if args or kwargs or opts: | |||||
args, kwargs, opts = self._merge(args, kwargs, opts) | |||||
else: | |||||
args, kwargs, opts = self.args, self.kwargs, self.options | |||||
s = Signature.from_dict({'task': self.task, 'args': tuple(args), | |||||
'kwargs': kwargs, 'options': deepcopy(opts), | |||||
'subtask_type': self.subtask_type, | |||||
'chord_size': self.chord_size, | |||||
'immutable': self.immutable}, | |||||
app=app or self._app) | |||||
s._type = self._type | |||||
return s | |||||
partial = clone | |||||
def freeze(self, _id=None, group_id=None, chord=None): | |||||
opts = self.options | |||||
try: | |||||
tid = opts['task_id'] | |||||
except KeyError: | |||||
tid = opts['task_id'] = _id or uuid() | |||||
if 'reply_to' not in opts: | |||||
opts['reply_to'] = self.app.oid | |||||
if group_id: | |||||
opts['group_id'] = group_id | |||||
if chord: | |||||
opts['chord'] = chord | |||||
return self.app.AsyncResult(tid) | |||||
_freeze = freeze | |||||
def replace(self, args=None, kwargs=None, options=None): | |||||
s = self.clone() | |||||
if args is not None: | |||||
s.args = args | |||||
if kwargs is not None: | |||||
s.kwargs = kwargs | |||||
if options is not None: | |||||
s.options = options | |||||
return s | |||||
def set(self, immutable=None, **options): | |||||
if immutable is not None: | |||||
self.set_immutable(immutable) | |||||
self.options.update(options) | |||||
return self | |||||
def set_immutable(self, immutable): | |||||
self.immutable = immutable | |||||
def apply_async(self, args=(), kwargs={}, **options): | |||||
try: | |||||
_apply = self._apply_async | |||||
except IndexError: # no tasks for chain, etc to find type | |||||
return | |||||
# For callbacks: extra args are prepended to the stored args. | |||||
if args or kwargs or options: | |||||
args, kwargs, options = self._merge(args, kwargs, options) | |||||
else: | |||||
args, kwargs, options = self.args, self.kwargs, self.options | |||||
return _apply(args, kwargs, **options) | |||||
def append_to_list_option(self, key, value): | |||||
items = self.options.setdefault(key, []) | |||||
if not isinstance(items, MutableSequence): | |||||
items = self.options[key] = [items] | |||||
if value not in items: | |||||
items.append(value) | |||||
return value | |||||
def link(self, callback): | |||||
return self.append_to_list_option('link', callback) | |||||
def link_error(self, errback): | |||||
return self.append_to_list_option('link_error', errback) | |||||
def flatten_links(self): | |||||
return list(_chain.from_iterable(_chain( | |||||
[[self]], | |||||
(link.flatten_links() | |||||
for link in maybe_list(self.options.get('link')) or []) | |||||
))) | |||||
def __or__(self, other): | |||||
if isinstance(other, group): | |||||
other = maybe_unroll_group(other) | |||||
if not isinstance(self, chain) and isinstance(other, chain): | |||||
return chain((self, ) + other.tasks, app=self._app) | |||||
elif isinstance(other, chain): | |||||
return chain(*self.tasks + other.tasks, app=self._app) | |||||
elif isinstance(other, Signature): | |||||
if isinstance(self, chain): | |||||
return chain(*self.tasks + (other, ), app=self._app) | |||||
return chain(self, other, app=self._app) | |||||
return NotImplemented | |||||
def __deepcopy__(self, memo): | |||||
memo[id(self)] = self | |||||
return dict(self) | |||||
def __invert__(self): | |||||
return self.apply_async().get() | |||||
def __reduce__(self): | |||||
# for serialization, the task type is lazily loaded, | |||||
# and not stored in the dict itself. | |||||
return subtask, (dict(self), ) | |||||
def reprcall(self, *args, **kwargs): | |||||
args, kwargs, _ = self._merge(args, kwargs, {}) | |||||
return reprcall(self['task'], args, kwargs) | |||||
def election(self): | |||||
type = self.type | |||||
app = type.app | |||||
tid = self.options.get('task_id') or uuid() | |||||
with app.producer_or_acquire(None) as P: | |||||
props = type.backend.on_task_call(P, tid) | |||||
app.control.election(tid, 'task', self.clone(task_id=tid, **props), | |||||
connection=P.connection) | |||||
return type.AsyncResult(tid) | |||||
def __repr__(self): | |||||
return self.reprcall() | |||||
@cached_property | |||||
def type(self): | |||||
return self._type or self.app.tasks[self['task']] | |||||
@cached_property | |||||
def app(self): | |||||
return self._app or current_app | |||||
@cached_property | |||||
def AsyncResult(self): | |||||
try: | |||||
return self.type.AsyncResult | |||||
except KeyError: # task not registered | |||||
return self.app.AsyncResult | |||||
@cached_property | |||||
def _apply_async(self): | |||||
try: | |||||
return self.type.apply_async | |||||
except KeyError: | |||||
return _partial(self.app.send_task, self['task']) | |||||
id = _getitem_property('options.task_id') | |||||
task = _getitem_property('task') | |||||
args = _getitem_property('args') | |||||
kwargs = _getitem_property('kwargs') | |||||
options = _getitem_property('options') | |||||
subtask_type = _getitem_property('subtask_type') | |||||
chord_size = _getitem_property('chord_size') | |||||
immutable = _getitem_property('immutable') | |||||
@Signature.register_type | |||||
class chain(Signature): | |||||
def __init__(self, *tasks, **options): | |||||
tasks = (regen(tasks[0]) if len(tasks) == 1 and is_list(tasks[0]) | |||||
else tasks) | |||||
Signature.__init__( | |||||
self, 'celery.chain', (), {'tasks': tasks}, **options | |||||
) | |||||
self.tasks = tasks | |||||
self.subtask_type = 'chain' | |||||
def __call__(self, *args, **kwargs): | |||||
if self.tasks: | |||||
return self.apply_async(args, kwargs) | |||||
@classmethod | |||||
def from_dict(self, d, app=None): | |||||
tasks = [maybe_signature(t, app=app) for t in d['kwargs']['tasks']] | |||||
if d['args'] and tasks: | |||||
# partial args passed on to first task in chain (Issue #1057). | |||||
tasks[0]['args'] = tasks[0]._merge(d['args'])[0] | |||||
return _upgrade(d, chain(*tasks, app=app, **d['options'])) | |||||
@property | |||||
def type(self): | |||||
try: | |||||
return self._type or self.tasks[0].type.app.tasks['celery.chain'] | |||||
except KeyError: | |||||
return self.app.tasks['celery.chain'] | |||||
def __repr__(self): | |||||
return ' | '.join(repr(t) for t in self.tasks) | |||||
class _basemap(Signature): | |||||
_task_name = None | |||||
_unpack_args = itemgetter('task', 'it') | |||||
def __init__(self, task, it, **options): | |||||
Signature.__init__( | |||||
self, self._task_name, (), | |||||
{'task': task, 'it': regen(it)}, immutable=True, **options | |||||
) | |||||
def apply_async(self, args=(), kwargs={}, **opts): | |||||
# need to evaluate generators | |||||
task, it = self._unpack_args(self.kwargs) | |||||
return self.type.apply_async( | |||||
(), {'task': task, 'it': list(it)}, **opts | |||||
) | |||||
@classmethod | |||||
def from_dict(cls, d, app=None): | |||||
return _upgrade( | |||||
d, cls(*cls._unpack_args(d['kwargs']), app=app, **d['options']), | |||||
) | |||||
@Signature.register_type | |||||
class xmap(_basemap): | |||||
_task_name = 'celery.map' | |||||
def __repr__(self): | |||||
task, it = self._unpack_args(self.kwargs) | |||||
return '[{0}(x) for x in {1}]'.format(task.task, | |||||
truncate(repr(it), 100)) | |||||
@Signature.register_type | |||||
class xstarmap(_basemap): | |||||
_task_name = 'celery.starmap' | |||||
def __repr__(self): | |||||
task, it = self._unpack_args(self.kwargs) | |||||
return '[{0}(*x) for x in {1}]'.format(task.task, | |||||
truncate(repr(it), 100)) | |||||
@Signature.register_type | |||||
class chunks(Signature): | |||||
_unpack_args = itemgetter('task', 'it', 'n') | |||||
def __init__(self, task, it, n, **options): | |||||
Signature.__init__( | |||||
self, 'celery.chunks', (), | |||||
{'task': task, 'it': regen(it), 'n': n}, | |||||
immutable=True, **options | |||||
) | |||||
@classmethod | |||||
def from_dict(self, d, app=None): | |||||
return _upgrade( | |||||
d, chunks(*self._unpack_args( | |||||
d['kwargs']), app=app, **d['options']), | |||||
) | |||||
def apply_async(self, args=(), kwargs={}, **opts): | |||||
return self.group().apply_async(args, kwargs, **opts) | |||||
def __call__(self, **options): | |||||
return self.group()(**options) | |||||
def group(self): | |||||
# need to evaluate generators | |||||
task, it, n = self._unpack_args(self.kwargs) | |||||
return group((xstarmap(task, part, app=self._app) | |||||
for part in _chunks(iter(it), n)), | |||||
app=self._app) | |||||
@classmethod | |||||
def apply_chunks(cls, task, it, n, app=None): | |||||
return cls(task, it, n, app=app)() | |||||
def _maybe_group(tasks): | |||||
if isinstance(tasks, group): | |||||
tasks = list(tasks.tasks) | |||||
elif isinstance(tasks, Signature): | |||||
tasks = [tasks] | |||||
else: | |||||
tasks = regen(tasks) | |||||
return tasks | |||||
def _maybe_clone(tasks, app): | |||||
return [s.clone() if isinstance(s, Signature) else signature(s, app=app) | |||||
for s in tasks] | |||||
@Signature.register_type | |||||
class group(Signature): | |||||
def __init__(self, *tasks, **options): | |||||
if len(tasks) == 1: | |||||
tasks = _maybe_group(tasks[0]) | |||||
Signature.__init__( | |||||
self, 'celery.group', (), {'tasks': tasks}, **options | |||||
) | |||||
self.tasks, self.subtask_type = tasks, 'group' | |||||
@classmethod | |||||
def from_dict(self, d, app=None): | |||||
tasks = [maybe_signature(t, app=app) for t in d['kwargs']['tasks']] | |||||
if d['args'] and tasks: | |||||
# partial args passed on to all tasks in the group (Issue #1057). | |||||
for task in tasks: | |||||
task['args'] = task._merge(d['args'])[0] | |||||
return _upgrade(d, group(tasks, app=app, **kwdict(d['options']))) | |||||
def apply_async(self, args=(), kwargs=None, add_to_parent=True, **options): | |||||
tasks = _maybe_clone(self.tasks, app=self._app) | |||||
if not tasks: | |||||
return self.freeze() | |||||
type = self.type | |||||
return type(*type.prepare(dict(self.options, **options), tasks, args), | |||||
add_to_parent=add_to_parent) | |||||
def set_immutable(self, immutable): | |||||
for task in self.tasks: | |||||
task.set_immutable(immutable) | |||||
def link(self, sig): | |||||
# Simply link to first task | |||||
sig = sig.clone().set(immutable=True) | |||||
return self.tasks[0].link(sig) | |||||
def link_error(self, sig): | |||||
sig = sig.clone().set(immutable=True) | |||||
return self.tasks[0].link_error(sig) | |||||
def apply(self, *args, **kwargs): | |||||
if not self.tasks: | |||||
return self.freeze() # empty group returns GroupResult | |||||
return Signature.apply(self, *args, **kwargs) | |||||
def __call__(self, *partial_args, **options): | |||||
return self.apply_async(partial_args, **options) | |||||
def freeze(self, _id=None, group_id=None, chord=None): | |||||
opts = self.options | |||||
try: | |||||
gid = opts['task_id'] | |||||
except KeyError: | |||||
gid = opts['task_id'] = uuid() | |||||
if group_id: | |||||
opts['group_id'] = group_id | |||||
if chord: | |||||
opts['chord'] = group_id | |||||
new_tasks, results = [], [] | |||||
for task in self.tasks: | |||||
task = maybe_signature(task, app=self._app).clone() | |||||
results.append(task.freeze(group_id=group_id, chord=chord)) | |||||
new_tasks.append(task) | |||||
self.tasks = self.kwargs['tasks'] = new_tasks | |||||
return self.app.GroupResult(gid, results) | |||||
_freeze = freeze | |||||
def skew(self, start=1.0, stop=None, step=1.0): | |||||
it = fxrange(start, stop, step, repeatlast=True) | |||||
for task in self.tasks: | |||||
task.set(countdown=next(it)) | |||||
return self | |||||
def __iter__(self): | |||||
return iter(self.tasks) | |||||
def __repr__(self): | |||||
return repr(self.tasks) | |||||
@property | |||||
def app(self): | |||||
return self._app or (self.tasks[0].app if self.tasks else current_app) | |||||
@property | |||||
def type(self): | |||||
if self._type: | |||||
return self._type | |||||
# taking the app from the first task in the list, there may be a | |||||
# better solution for this, e.g. to consolidate tasks with the same | |||||
# app and apply them in batches. | |||||
return self.app.tasks[self['task']] | |||||
@Signature.register_type | |||||
class chord(Signature): | |||||
def __init__(self, header, body=None, task='celery.chord', | |||||
args=(), kwargs={}, **options): | |||||
Signature.__init__( | |||||
self, task, args, | |||||
dict(kwargs, header=_maybe_group(header), | |||||
body=maybe_signature(body, app=self._app)), **options | |||||
) | |||||
self.subtask_type = 'chord' | |||||
def apply(self, args=(), kwargs={}, **options): | |||||
# For callbacks: extra args are prepended to the stored args. | |||||
args, kwargs, options = self._merge(args, kwargs, options) | |||||
return self.type.apply(args, kwargs, **options) | |||||
def freeze(self, _id=None, group_id=None, chord=None): | |||||
return self.body.freeze(_id, group_id=group_id, chord=chord) | |||||
@classmethod | |||||
def from_dict(self, d, app=None): | |||||
args, d['kwargs'] = self._unpack_args(**kwdict(d['kwargs'])) | |||||
return _upgrade(d, self(*args, app=app, **kwdict(d))) | |||||
@staticmethod | |||||
def _unpack_args(header=None, body=None, **kwargs): | |||||
# Python signatures are better at extracting keys from dicts | |||||
# than manually popping things off. | |||||
return (header, body), kwargs | |||||
@property | |||||
def app(self): | |||||
# we will be able to fix this mess in 3.2 when we no longer | |||||
# require an actual task implementation for chord/group | |||||
if self._app: | |||||
return self._app | |||||
app = None if self.body is None else self.body.app | |||||
if app is None: | |||||
try: | |||||
app = self.tasks[0].app | |||||
except IndexError: | |||||
app = None | |||||
return app if app is not None else current_app | |||||
@property | |||||
def type(self): | |||||
if self._type: | |||||
return self._type | |||||
return self.app.tasks['celery.chord'] | |||||
def delay(self, *partial_args, **partial_kwargs): | |||||
# There's no partial_kwargs for chord. | |||||
return self.apply_async(partial_args) | |||||
def apply_async(self, args=(), kwargs={}, task_id=None, | |||||
producer=None, publisher=None, connection=None, | |||||
router=None, result_cls=None, **options): | |||||
args = (tuple(args) + tuple(self.args) | |||||
if args and not self.immutable else self.args) | |||||
body = kwargs.get('body') or self.kwargs['body'] | |||||
kwargs = dict(self.kwargs, **kwargs) | |||||
body = body.clone(**options) | |||||
_chord = self.type | |||||
if _chord.app.conf.CELERY_ALWAYS_EAGER: | |||||
return self.apply(args, kwargs, task_id=task_id, **options) | |||||
res = body.freeze(task_id) | |||||
parent = _chord(self.tasks, body, args, **options) | |||||
res.parent = parent | |||||
return res | |||||
def __call__(self, body=None, **options): | |||||
return self.apply_async( | |||||
(), {'body': body} if body else {}, **options) | |||||
def clone(self, *args, **kwargs): | |||||
s = Signature.clone(self, *args, **kwargs) | |||||
# need to make copy of body | |||||
try: | |||||
s.kwargs['body'] = s.kwargs['body'].clone() | |||||
except (AttributeError, KeyError): | |||||
pass | |||||
return s | |||||
def link(self, callback): | |||||
self.body.link(callback) | |||||
return callback | |||||
def link_error(self, errback): | |||||
self.body.link_error(errback) | |||||
return errback | |||||
def set_immutable(self, immutable): | |||||
# changes mutability of header only, not callback. | |||||
for task in self.tasks: | |||||
task.set_immutable(immutable) | |||||
def __repr__(self): | |||||
if self.body: | |||||
return self.body.reprcall(self.tasks) | |||||
return '<chord without body: {0.tasks!r}>'.format(self) | |||||
tasks = _getitem_property('kwargs.header') | |||||
body = _getitem_property('kwargs.body') | |||||
def signature(varies, args=(), kwargs={}, options={}, app=None, **kw): | |||||
if isinstance(varies, dict): | |||||
if isinstance(varies, Signature): | |||||
return varies.clone(app=app) | |||||
return Signature.from_dict(varies, app=app) | |||||
return Signature(varies, args, kwargs, options, app=app, **kw) | |||||
subtask = signature # XXX compat | |||||
def maybe_signature(d, app=None): | |||||
if d is not None: | |||||
if isinstance(d, dict): | |||||
if not isinstance(d, Signature): | |||||
return signature(d, app=app) | |||||
elif isinstance(d, list): | |||||
return [maybe_signature(s, app=app) for s in d] | |||||
if app is not None: | |||||
d._app = app | |||||
return d | |||||
maybe_subtask = maybe_signature # XXX compat |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.concurrency | |||||
~~~~~~~~~~~~~~~~~~ | |||||
Pool implementation abstract factory, and alias definitions. | |||||
""" | |||||
from __future__ import absolute_import | |||||
# Import from kombu directly as it's used | |||||
# early in the import stage, where celery.utils loads | |||||
# too much (e.g. for eventlet patching) | |||||
from kombu.utils import symbol_by_name | |||||
__all__ = ['get_implementation'] | |||||
ALIASES = { | |||||
'prefork': 'celery.concurrency.prefork:TaskPool', | |||||
'eventlet': 'celery.concurrency.eventlet:TaskPool', | |||||
'gevent': 'celery.concurrency.gevent:TaskPool', | |||||
'threads': 'celery.concurrency.threads:TaskPool', | |||||
'solo': 'celery.concurrency.solo:TaskPool', | |||||
'processes': 'celery.concurrency.prefork:TaskPool', # XXX compat alias | |||||
} | |||||
def get_implementation(cls): | |||||
return symbol_by_name(cls, ALIASES) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.concurrency.base | |||||
~~~~~~~~~~~~~~~~~~~~~~~ | |||||
TaskPool interface. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import logging | |||||
import os | |||||
import sys | |||||
from billiard.einfo import ExceptionInfo | |||||
from billiard.exceptions import WorkerLostError | |||||
from kombu.utils.encoding import safe_repr | |||||
from celery.exceptions import WorkerShutdown, WorkerTerminate | |||||
from celery.five import monotonic, reraise | |||||
from celery.utils import timer2 | |||||
from celery.utils.text import truncate | |||||
from celery.utils.log import get_logger | |||||
__all__ = ['BasePool', 'apply_target'] | |||||
logger = get_logger('celery.pool') | |||||
def apply_target(target, args=(), kwargs={}, callback=None, | |||||
accept_callback=None, pid=None, getpid=os.getpid, | |||||
propagate=(), monotonic=monotonic, **_): | |||||
if accept_callback: | |||||
accept_callback(pid or getpid(), monotonic()) | |||||
try: | |||||
ret = target(*args, **kwargs) | |||||
except propagate: | |||||
raise | |||||
except Exception: | |||||
raise | |||||
except (WorkerShutdown, WorkerTerminate): | |||||
raise | |||||
except BaseException as exc: | |||||
try: | |||||
reraise(WorkerLostError, WorkerLostError(repr(exc)), | |||||
sys.exc_info()[2]) | |||||
except WorkerLostError: | |||||
callback(ExceptionInfo()) | |||||
else: | |||||
callback(ret) | |||||
class BasePool(object): | |||||
RUN = 0x1 | |||||
CLOSE = 0x2 | |||||
TERMINATE = 0x3 | |||||
Timer = timer2.Timer | |||||
#: set to true if the pool can be shutdown from within | |||||
#: a signal handler. | |||||
signal_safe = True | |||||
#: set to true if pool uses greenlets. | |||||
is_green = False | |||||
_state = None | |||||
_pool = None | |||||
#: only used by multiprocessing pool | |||||
uses_semaphore = False | |||||
task_join_will_block = True | |||||
def __init__(self, limit=None, putlocks=True, | |||||
forking_enable=True, callbacks_propagate=(), **options): | |||||
self.limit = limit | |||||
self.putlocks = putlocks | |||||
self.options = options | |||||
self.forking_enable = forking_enable | |||||
self.callbacks_propagate = callbacks_propagate | |||||
self._does_debug = logger.isEnabledFor(logging.DEBUG) | |||||
def on_start(self): | |||||
pass | |||||
def did_start_ok(self): | |||||
return True | |||||
def flush(self): | |||||
pass | |||||
def on_stop(self): | |||||
pass | |||||
def register_with_event_loop(self, loop): | |||||
pass | |||||
def on_apply(self, *args, **kwargs): | |||||
pass | |||||
def on_terminate(self): | |||||
pass | |||||
def on_soft_timeout(self, job): | |||||
pass | |||||
def on_hard_timeout(self, job): | |||||
pass | |||||
def maintain_pool(self, *args, **kwargs): | |||||
pass | |||||
def terminate_job(self, pid, signal=None): | |||||
raise NotImplementedError( | |||||
'{0} does not implement kill_job'.format(type(self))) | |||||
def restart(self): | |||||
raise NotImplementedError( | |||||
'{0} does not implement restart'.format(type(self))) | |||||
def stop(self): | |||||
self.on_stop() | |||||
self._state = self.TERMINATE | |||||
def terminate(self): | |||||
self._state = self.TERMINATE | |||||
self.on_terminate() | |||||
def start(self): | |||||
self.on_start() | |||||
self._state = self.RUN | |||||
def close(self): | |||||
self._state = self.CLOSE | |||||
self.on_close() | |||||
def on_close(self): | |||||
pass | |||||
def apply_async(self, target, args=[], kwargs={}, **options): | |||||
"""Equivalent of the :func:`apply` built-in function. | |||||
Callbacks should optimally return as soon as possible since | |||||
otherwise the thread which handles the result will get blocked. | |||||
""" | |||||
if self._does_debug: | |||||
logger.debug('TaskPool: Apply %s (args:%s kwargs:%s)', | |||||
target, truncate(safe_repr(args), 1024), | |||||
truncate(safe_repr(kwargs), 1024)) | |||||
return self.on_apply(target, args, kwargs, | |||||
waitforslot=self.putlocks, | |||||
callbacks_propagate=self.callbacks_propagate, | |||||
**options) | |||||
def _get_info(self): | |||||
return {} | |||||
@property | |||||
def info(self): | |||||
return self._get_info() | |||||
@property | |||||
def active(self): | |||||
return self._state == self.RUN | |||||
@property | |||||
def num_processes(self): | |||||
return self.limit |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.concurrency.eventlet | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Eventlet pool implementation. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import sys | |||||
from time import time | |||||
__all__ = ['TaskPool'] | |||||
W_RACE = """\ | |||||
Celery module with %s imported before eventlet patched\ | |||||
""" | |||||
RACE_MODS = ('billiard.', 'celery.', 'kombu.') | |||||
#: Warn if we couldn't patch early enough, | |||||
#: and thread/socket depending celery modules have already been loaded. | |||||
for mod in (mod for mod in sys.modules if mod.startswith(RACE_MODS)): | |||||
for side in ('thread', 'threading', 'socket'): # pragma: no cover | |||||
if getattr(mod, side, None): | |||||
import warnings | |||||
warnings.warn(RuntimeWarning(W_RACE % side)) | |||||
from celery import signals # noqa | |||||
from celery.utils import timer2 # noqa | |||||
from . import base # noqa | |||||
def apply_target(target, args=(), kwargs={}, callback=None, | |||||
accept_callback=None, getpid=None): | |||||
return base.apply_target(target, args, kwargs, callback, accept_callback, | |||||
pid=getpid()) | |||||
class Schedule(timer2.Schedule): | |||||
def __init__(self, *args, **kwargs): | |||||
from eventlet.greenthread import spawn_after | |||||
from greenlet import GreenletExit | |||||
super(Schedule, self).__init__(*args, **kwargs) | |||||
self.GreenletExit = GreenletExit | |||||
self._spawn_after = spawn_after | |||||
self._queue = set() | |||||
def _enter(self, eta, priority, entry): | |||||
secs = max(eta - time(), 0) | |||||
g = self._spawn_after(secs, entry) | |||||
self._queue.add(g) | |||||
g.link(self._entry_exit, entry) | |||||
g.entry = entry | |||||
g.eta = eta | |||||
g.priority = priority | |||||
g.canceled = False | |||||
return g | |||||
def _entry_exit(self, g, entry): | |||||
try: | |||||
try: | |||||
g.wait() | |||||
except self.GreenletExit: | |||||
entry.cancel() | |||||
g.canceled = True | |||||
finally: | |||||
self._queue.discard(g) | |||||
def clear(self): | |||||
queue = self._queue | |||||
while queue: | |||||
try: | |||||
queue.pop().cancel() | |||||
except (KeyError, self.GreenletExit): | |||||
pass | |||||
@property | |||||
def queue(self): | |||||
return self._queue | |||||
class Timer(timer2.Timer): | |||||
Schedule = Schedule | |||||
def ensure_started(self): | |||||
pass | |||||
def stop(self): | |||||
self.schedule.clear() | |||||
def cancel(self, tref): | |||||
try: | |||||
tref.cancel() | |||||
except self.schedule.GreenletExit: | |||||
pass | |||||
def start(self): | |||||
pass | |||||
class TaskPool(base.BasePool): | |||||
Timer = Timer | |||||
signal_safe = False | |||||
is_green = True | |||||
task_join_will_block = False | |||||
def __init__(self, *args, **kwargs): | |||||
from eventlet import greenthread | |||||
from eventlet.greenpool import GreenPool | |||||
self.Pool = GreenPool | |||||
self.getcurrent = greenthread.getcurrent | |||||
self.getpid = lambda: id(greenthread.getcurrent()) | |||||
self.spawn_n = greenthread.spawn_n | |||||
super(TaskPool, self).__init__(*args, **kwargs) | |||||
def on_start(self): | |||||
self._pool = self.Pool(self.limit) | |||||
signals.eventlet_pool_started.send(sender=self) | |||||
self._quick_put = self._pool.spawn_n | |||||
self._quick_apply_sig = signals.eventlet_pool_apply.send | |||||
def on_stop(self): | |||||
signals.eventlet_pool_preshutdown.send(sender=self) | |||||
if self._pool is not None: | |||||
self._pool.waitall() | |||||
signals.eventlet_pool_postshutdown.send(sender=self) | |||||
def on_apply(self, target, args=None, kwargs=None, callback=None, | |||||
accept_callback=None, **_): | |||||
self._quick_apply_sig( | |||||
sender=self, target=target, args=args, kwargs=kwargs, | |||||
) | |||||
self._quick_put(apply_target, target, args, kwargs, | |||||
callback, accept_callback, | |||||
self.getpid) | |||||
def grow(self, n=1): | |||||
limit = self.limit + n | |||||
self._pool.resize(limit) | |||||
self.limit = limit | |||||
def shrink(self, n=1): | |||||
limit = self.limit - n | |||||
self._pool.resize(limit) | |||||
self.limit = limit | |||||
def _get_info(self): | |||||
return { | |||||
'max-concurrency': self.limit, | |||||
'free-threads': self._pool.free(), | |||||
'running-threads': self._pool.running(), | |||||
} |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.concurrency.gevent | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
gevent pool implementation. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from time import time | |||||
try: | |||||
from gevent import Timeout | |||||
except ImportError: # pragma: no cover | |||||
Timeout = None # noqa | |||||
from celery.utils import timer2 | |||||
from .base import apply_target, BasePool | |||||
__all__ = ['TaskPool'] | |||||
def apply_timeout(target, args=(), kwargs={}, callback=None, | |||||
accept_callback=None, pid=None, timeout=None, | |||||
timeout_callback=None, Timeout=Timeout, | |||||
apply_target=apply_target, **rest): | |||||
try: | |||||
with Timeout(timeout): | |||||
return apply_target(target, args, kwargs, callback, | |||||
accept_callback, pid, | |||||
propagate=(Timeout, ), **rest) | |||||
except Timeout: | |||||
return timeout_callback(False, timeout) | |||||
class Schedule(timer2.Schedule): | |||||
def __init__(self, *args, **kwargs): | |||||
from gevent.greenlet import Greenlet, GreenletExit | |||||
class _Greenlet(Greenlet): | |||||
cancel = Greenlet.kill | |||||
self._Greenlet = _Greenlet | |||||
self._GreenletExit = GreenletExit | |||||
super(Schedule, self).__init__(*args, **kwargs) | |||||
self._queue = set() | |||||
def _enter(self, eta, priority, entry): | |||||
secs = max(eta - time(), 0) | |||||
g = self._Greenlet.spawn_later(secs, entry) | |||||
self._queue.add(g) | |||||
g.link(self._entry_exit) | |||||
g.entry = entry | |||||
g.eta = eta | |||||
g.priority = priority | |||||
g.canceled = False | |||||
return g | |||||
def _entry_exit(self, g): | |||||
try: | |||||
g.kill() | |||||
finally: | |||||
self._queue.discard(g) | |||||
def clear(self): | |||||
queue = self._queue | |||||
while queue: | |||||
try: | |||||
queue.pop().kill() | |||||
except KeyError: | |||||
pass | |||||
@property | |||||
def queue(self): | |||||
return self._queue | |||||
class Timer(timer2.Timer): | |||||
Schedule = Schedule | |||||
def ensure_started(self): | |||||
pass | |||||
def stop(self): | |||||
self.schedule.clear() | |||||
def start(self): | |||||
pass | |||||
class TaskPool(BasePool): | |||||
Timer = Timer | |||||
signal_safe = False | |||||
is_green = True | |||||
task_join_will_block = False | |||||
def __init__(self, *args, **kwargs): | |||||
from gevent import spawn_raw | |||||
from gevent.pool import Pool | |||||
self.Pool = Pool | |||||
self.spawn_n = spawn_raw | |||||
self.timeout = kwargs.get('timeout') | |||||
super(TaskPool, self).__init__(*args, **kwargs) | |||||
def on_start(self): | |||||
self._pool = self.Pool(self.limit) | |||||
self._quick_put = self._pool.spawn | |||||
def on_stop(self): | |||||
if self._pool is not None: | |||||
self._pool.join() | |||||
def on_apply(self, target, args=None, kwargs=None, callback=None, | |||||
accept_callback=None, timeout=None, | |||||
timeout_callback=None, **_): | |||||
timeout = self.timeout if timeout is None else timeout | |||||
return self._quick_put(apply_timeout if timeout else apply_target, | |||||
target, args, kwargs, callback, accept_callback, | |||||
timeout=timeout, | |||||
timeout_callback=timeout_callback) | |||||
def grow(self, n=1): | |||||
self._pool._semaphore.counter += n | |||||
self._pool.size += n | |||||
def shrink(self, n=1): | |||||
self._pool._semaphore.counter -= n | |||||
self._pool.size -= n | |||||
@property | |||||
def num_processes(self): | |||||
return len(self._pool) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.concurrency.prefork | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Pool implementation using :mod:`multiprocessing`. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import os | |||||
from billiard import forking_enable | |||||
from billiard.pool import RUN, CLOSE, Pool as BlockingPool | |||||
from celery import platforms | |||||
from celery import signals | |||||
from celery._state import set_default_app, _set_task_join_will_block | |||||
from celery.app import trace | |||||
from celery.concurrency.base import BasePool | |||||
from celery.five import items | |||||
from celery.utils.functional import noop | |||||
from celery.utils.log import get_logger | |||||
from .asynpool import AsynPool | |||||
__all__ = ['TaskPool', 'process_initializer', 'process_destructor'] | |||||
#: List of signals to reset when a child process starts. | |||||
WORKER_SIGRESET = frozenset(['SIGTERM', | |||||
'SIGHUP', | |||||
'SIGTTIN', | |||||
'SIGTTOU', | |||||
'SIGUSR1']) | |||||
#: List of signals to ignore when a child process starts. | |||||
WORKER_SIGIGNORE = frozenset(['SIGINT']) | |||||
logger = get_logger(__name__) | |||||
warning, debug = logger.warning, logger.debug | |||||
def process_initializer(app, hostname): | |||||
"""Pool child process initializer. | |||||
This will initialize a child pool process to ensure the correct | |||||
app instance is used and things like | |||||
logging works. | |||||
""" | |||||
_set_task_join_will_block(True) | |||||
platforms.signals.reset(*WORKER_SIGRESET) | |||||
platforms.signals.ignore(*WORKER_SIGIGNORE) | |||||
platforms.set_mp_process_title('celeryd', hostname=hostname) | |||||
# This is for Windows and other platforms not supporting | |||||
# fork(). Note that init_worker makes sure it's only | |||||
# run once per process. | |||||
app.loader.init_worker() | |||||
app.loader.init_worker_process() | |||||
logfile = os.environ.get('CELERY_LOG_FILE') or None | |||||
if logfile and '%i' in logfile.lower(): | |||||
# logfile path will differ so need to set up logging again. | |||||
app.log.already_setup = False | |||||
app.log.setup(int(os.environ.get('CELERY_LOG_LEVEL', 0) or 0), | |||||
logfile, | |||||
bool(os.environ.get('CELERY_LOG_REDIRECT', False)), | |||||
str(os.environ.get('CELERY_LOG_REDIRECT_LEVEL')), | |||||
hostname=hostname) | |||||
if os.environ.get('FORKED_BY_MULTIPROCESSING'): | |||||
# pool did execv after fork | |||||
trace.setup_worker_optimizations(app) | |||||
else: | |||||
app.set_current() | |||||
set_default_app(app) | |||||
app.finalize() | |||||
trace._tasks = app._tasks # enables fast_trace_task optimization. | |||||
# rebuild execution handler for all tasks. | |||||
from celery.app.trace import build_tracer | |||||
for name, task in items(app.tasks): | |||||
task.__trace__ = build_tracer(name, task, app.loader, hostname, | |||||
app=app) | |||||
from celery.worker import state as worker_state | |||||
worker_state.reset_state() | |||||
signals.worker_process_init.send(sender=None) | |||||
def process_destructor(pid, exitcode): | |||||
"""Pool child process destructor | |||||
Dispatch the :signal:`worker_process_shutdown` signal. | |||||
""" | |||||
signals.worker_process_shutdown.send( | |||||
sender=None, pid=pid, exitcode=exitcode, | |||||
) | |||||
class TaskPool(BasePool): | |||||
"""Multiprocessing Pool implementation.""" | |||||
Pool = AsynPool | |||||
BlockingPool = BlockingPool | |||||
uses_semaphore = True | |||||
write_stats = None | |||||
def on_start(self): | |||||
"""Run the task pool. | |||||
Will pre-fork all workers so they're ready to accept tasks. | |||||
""" | |||||
forking_enable(self.forking_enable) | |||||
Pool = (self.BlockingPool if self.options.get('threads', True) | |||||
else self.Pool) | |||||
P = self._pool = Pool(processes=self.limit, | |||||
initializer=process_initializer, | |||||
on_process_exit=process_destructor, | |||||
synack=False, | |||||
**self.options) | |||||
# Create proxy methods | |||||
self.on_apply = P.apply_async | |||||
self.maintain_pool = P.maintain_pool | |||||
self.terminate_job = P.terminate_job | |||||
self.grow = P.grow | |||||
self.shrink = P.shrink | |||||
self.flush = getattr(P, 'flush', None) # FIXME add to billiard | |||||
def restart(self): | |||||
self._pool.restart() | |||||
self._pool.apply_async(noop) | |||||
def did_start_ok(self): | |||||
return self._pool.did_start_ok() | |||||
def register_with_event_loop(self, loop): | |||||
try: | |||||
reg = self._pool.register_with_event_loop | |||||
except AttributeError: | |||||
return | |||||
return reg(loop) | |||||
def on_stop(self): | |||||
"""Gracefully stop the pool.""" | |||||
if self._pool is not None and self._pool._state in (RUN, CLOSE): | |||||
self._pool.close() | |||||
self._pool.join() | |||||
self._pool = None | |||||
def on_terminate(self): | |||||
"""Force terminate the pool.""" | |||||
if self._pool is not None: | |||||
self._pool.terminate() | |||||
self._pool = None | |||||
def on_close(self): | |||||
if self._pool is not None and self._pool._state == RUN: | |||||
self._pool.close() | |||||
def _get_info(self): | |||||
try: | |||||
write_stats = self._pool.human_write_stats | |||||
except AttributeError: | |||||
def write_stats(): | |||||
return 'N/A' # only supported by asynpool | |||||
return { | |||||
'max-concurrency': self.limit, | |||||
'processes': [p.pid for p in self._pool._pool], | |||||
'max-tasks-per-child': self._pool._maxtasksperchild or 'N/A', | |||||
'put-guarded-by-semaphore': self.putlocks, | |||||
'timeouts': (self._pool.soft_timeout or 0, | |||||
self._pool.timeout or 0), | |||||
'writes': write_stats() | |||||
} | |||||
@property | |||||
def num_processes(self): | |||||
return self._pool._processes |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.concurrency.solo | |||||
~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Single-threaded pool implementation. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import os | |||||
from .base import BasePool, apply_target | |||||
__all__ = ['TaskPool'] | |||||
class TaskPool(BasePool): | |||||
"""Solo task pool (blocking, inline, fast).""" | |||||
def __init__(self, *args, **kwargs): | |||||
super(TaskPool, self).__init__(*args, **kwargs) | |||||
self.on_apply = apply_target | |||||
def _get_info(self): | |||||
return {'max-concurrency': 1, | |||||
'processes': [os.getpid()], | |||||
'max-tasks-per-child': None, | |||||
'put-guarded-by-semaphore': True, | |||||
'timeouts': ()} |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.concurrency.threads | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Pool implementation using threads. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from celery.five import UserDict | |||||
from .base import apply_target, BasePool | |||||
__all__ = ['TaskPool'] | |||||
class NullDict(UserDict): | |||||
def __setitem__(self, key, value): | |||||
pass | |||||
class TaskPool(BasePool): | |||||
def __init__(self, *args, **kwargs): | |||||
try: | |||||
import threadpool | |||||
except ImportError: | |||||
raise ImportError( | |||||
'The threaded pool requires the threadpool module.') | |||||
self.WorkRequest = threadpool.WorkRequest | |||||
self.ThreadPool = threadpool.ThreadPool | |||||
super(TaskPool, self).__init__(*args, **kwargs) | |||||
def on_start(self): | |||||
self._pool = self.ThreadPool(self.limit) | |||||
# threadpool stores all work requests until they are processed | |||||
# we don't need this dict, and it occupies way too much memory. | |||||
self._pool.workRequests = NullDict() | |||||
self._quick_put = self._pool.putRequest | |||||
self._quick_clear = self._pool._results_queue.queue.clear | |||||
def on_stop(self): | |||||
self._pool.dismissWorkers(self.limit, do_join=True) | |||||
def on_apply(self, target, args=None, kwargs=None, callback=None, | |||||
accept_callback=None, **_): | |||||
req = self.WorkRequest(apply_target, (target, args, kwargs, callback, | |||||
accept_callback)) | |||||
self._quick_put(req) | |||||
# threadpool also has callback support, | |||||
# but for some reason the callback is not triggered | |||||
# before you've collected the results. | |||||
# Clear the results (if any), so it doesn't grow too large. | |||||
self._quick_clear() | |||||
return req |
# -*- coding: utf-8 -*- | |||||
""" | |||||
========================= | |||||
Abortable tasks overview | |||||
========================= | |||||
For long-running :class:`Task`'s, it can be desirable to support | |||||
aborting during execution. Of course, these tasks should be built to | |||||
support abortion specifically. | |||||
The :class:`AbortableTask` serves as a base class for all :class:`Task` | |||||
objects that should support abortion by producers. | |||||
* Producers may invoke the :meth:`abort` method on | |||||
:class:`AbortableAsyncResult` instances, to request abortion. | |||||
* Consumers (workers) should periodically check (and honor!) the | |||||
:meth:`is_aborted` method at controlled points in their task's | |||||
:meth:`run` method. The more often, the better. | |||||
The necessary intermediate communication is dealt with by the | |||||
:class:`AbortableTask` implementation. | |||||
Usage example | |||||
------------- | |||||
In the consumer: | |||||
.. code-block:: python | |||||
from __future__ import absolute_import | |||||
from celery.contrib.abortable import AbortableTask | |||||
from celery.utils.log import get_task_logger | |||||
from proj.celery import app | |||||
logger = get_logger(__name__) | |||||
@app.task(bind=True, base=AbortableTask) | |||||
def long_running_task(self): | |||||
results = [] | |||||
for i in range(100): | |||||
# check after every 5 iterations... | |||||
# (or alternatively, check when some timer is due) | |||||
if not i % 5: | |||||
if self.is_aborted(): | |||||
# respect aborted state, and terminate gracefully. | |||||
logger.warning('Task aborted') | |||||
return | |||||
value = do_something_expensive(i) | |||||
results.append(y) | |||||
logger.info('Task complete') | |||||
return results | |||||
In the producer: | |||||
.. code-block:: python | |||||
from __future__ import absolute_import | |||||
import time | |||||
from proj.tasks import MyLongRunningTask | |||||
def myview(request): | |||||
# result is of type AbortableAsyncResult | |||||
result = long_running_task.delay() | |||||
# abort the task after 10 seconds | |||||
time.sleep(10) | |||||
result.abort() | |||||
After the `result.abort()` call, the task execution is not | |||||
aborted immediately. In fact, it is not guaranteed to abort at all. Keep | |||||
checking `result.state` status, or call `result.get(timeout=)` to | |||||
have it block until the task is finished. | |||||
.. note:: | |||||
In order to abort tasks, there needs to be communication between the | |||||
producer and the consumer. This is currently implemented through the | |||||
database backend. Therefore, this class will only work with the | |||||
database backends. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from celery import Task | |||||
from celery.result import AsyncResult | |||||
__all__ = ['AbortableAsyncResult', 'AbortableTask'] | |||||
""" | |||||
Task States | |||||
----------- | |||||
.. state:: ABORTED | |||||
ABORTED | |||||
~~~~~~~ | |||||
Task is aborted (typically by the producer) and should be | |||||
aborted as soon as possible. | |||||
""" | |||||
ABORTED = 'ABORTED' | |||||
class AbortableAsyncResult(AsyncResult): | |||||
"""Represents a abortable result. | |||||
Specifically, this gives the `AsyncResult` a :meth:`abort()` method, | |||||
which sets the state of the underlying Task to `'ABORTED'`. | |||||
""" | |||||
def is_aborted(self): | |||||
"""Return :const:`True` if the task is (being) aborted.""" | |||||
return self.state == ABORTED | |||||
def abort(self): | |||||
"""Set the state of the task to :const:`ABORTED`. | |||||
Abortable tasks monitor their state at regular intervals and | |||||
terminate execution if so. | |||||
Be aware that invoking this method does not guarantee when the | |||||
task will be aborted (or even if the task will be aborted at | |||||
all). | |||||
""" | |||||
# TODO: store_result requires all four arguments to be set, | |||||
# but only status should be updated here | |||||
return self.backend.store_result(self.id, result=None, | |||||
status=ABORTED, traceback=None) | |||||
class AbortableTask(Task): | |||||
"""A celery task that serves as a base class for all :class:`Task`'s | |||||
that support aborting during execution. | |||||
All subclasses of :class:`AbortableTask` must call the | |||||
:meth:`is_aborted` method periodically and act accordingly when | |||||
the call evaluates to :const:`True`. | |||||
""" | |||||
abstract = True | |||||
def AsyncResult(self, task_id): | |||||
"""Return the accompanying AbortableAsyncResult instance.""" | |||||
return AbortableAsyncResult(task_id, backend=self.backend) | |||||
def is_aborted(self, **kwargs): | |||||
"""Checks against the backend whether this | |||||
:class:`AbortableAsyncResult` is :const:`ABORTED`. | |||||
Always return :const:`False` in case the `task_id` parameter | |||||
refers to a regular (non-abortable) :class:`Task`. | |||||
Be aware that invoking this method will cause a hit in the | |||||
backend (for example a database query), so find a good balance | |||||
between calling it regularly (for responsiveness), but not too | |||||
often (for performance). | |||||
""" | |||||
task_id = kwargs.get('task_id', self.request.id) | |||||
result = self.AsyncResult(task_id) | |||||
if not isinstance(result, AbortableAsyncResult): | |||||
return False | |||||
return result.is_aborted() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.contrib.batches | |||||
====================== | |||||
Experimental task class that buffers messages and processes them as a list. | |||||
.. warning:: | |||||
For this to work you have to set | |||||
:setting:`CELERYD_PREFETCH_MULTIPLIER` to zero, or some value where | |||||
the final multiplied value is higher than ``flush_every``. | |||||
In the future we hope to add the ability to direct batching tasks | |||||
to a channel with different QoS requirements than the task channel. | |||||
**Simple Example** | |||||
A click counter that flushes the buffer every 100 messages, and every | |||||
10 seconds. Does not do anything with the data, but can easily be modified | |||||
to store it in a database. | |||||
.. code-block:: python | |||||
# Flush after 100 messages, or 10 seconds. | |||||
@app.task(base=Batches, flush_every=100, flush_interval=10) | |||||
def count_click(requests): | |||||
from collections import Counter | |||||
count = Counter(request.kwargs['url'] for request in requests) | |||||
for url, count in count.items(): | |||||
print('>>> Clicks: {0} -> {1}'.format(url, count)) | |||||
Then you can ask for a click to be counted by doing:: | |||||
>>> count_click.delay(url='http://example.com') | |||||
**Example returning results** | |||||
An interface to the Web of Trust API that flushes the buffer every 100 | |||||
messages, and every 10 seconds. | |||||
.. code-block:: python | |||||
import requests | |||||
from urlparse import urlparse | |||||
from celery.contrib.batches import Batches | |||||
wot_api_target = 'https://api.mywot.com/0.4/public_link_json' | |||||
@app.task(base=Batches, flush_every=100, flush_interval=10) | |||||
def wot_api(requests): | |||||
sig = lambda url: url | |||||
reponses = wot_api_real( | |||||
(sig(*request.args, **request.kwargs) for request in requests) | |||||
) | |||||
# use mark_as_done to manually return response data | |||||
for response, request in zip(reponses, requests): | |||||
app.backend.mark_as_done(request.id, response) | |||||
def wot_api_real(urls): | |||||
domains = [urlparse(url).netloc for url in urls] | |||||
response = requests.get( | |||||
wot_api_target, | |||||
params={'hosts': ('/').join(set(domains)) + '/'} | |||||
) | |||||
return [response.json()[domain] for domain in domains] | |||||
Using the API is done as follows:: | |||||
>>> wot_api.delay('http://example.com') | |||||
.. note:: | |||||
If you don't have an ``app`` instance then use the current app proxy | |||||
instead:: | |||||
from celery import current_app | |||||
app.backend.mark_as_done(request.id, response) | |||||
""" | |||||
from __future__ import absolute_import | |||||
from itertools import count | |||||
from celery.task import Task | |||||
from celery.five import Empty, Queue | |||||
from celery.utils.log import get_logger | |||||
from celery.worker.job import Request | |||||
from celery.utils import noop | |||||
__all__ = ['Batches'] | |||||
logger = get_logger(__name__) | |||||
def consume_queue(queue): | |||||
"""Iterator yielding all immediately available items in a | |||||
:class:`Queue.Queue`. | |||||
The iterator stops as soon as the queue raises :exc:`Queue.Empty`. | |||||
*Examples* | |||||
>>> q = Queue() | |||||
>>> map(q.put, range(4)) | |||||
>>> list(consume_queue(q)) | |||||
[0, 1, 2, 3] | |||||
>>> list(consume_queue(q)) | |||||
[] | |||||
""" | |||||
get = queue.get_nowait | |||||
while 1: | |||||
try: | |||||
yield get() | |||||
except Empty: | |||||
break | |||||
def apply_batches_task(task, args, loglevel, logfile): | |||||
task.push_request(loglevel=loglevel, logfile=logfile) | |||||
try: | |||||
result = task(*args) | |||||
except Exception as exc: | |||||
result = None | |||||
logger.error('Error: %r', exc, exc_info=True) | |||||
finally: | |||||
task.pop_request() | |||||
return result | |||||
class SimpleRequest(object): | |||||
"""Pickleable request.""" | |||||
#: task id | |||||
id = None | |||||
#: task name | |||||
name = None | |||||
#: positional arguments | |||||
args = () | |||||
#: keyword arguments | |||||
kwargs = {} | |||||
#: message delivery information. | |||||
delivery_info = None | |||||
#: worker node name | |||||
hostname = None | |||||
def __init__(self, id, name, args, kwargs, delivery_info, hostname): | |||||
self.id = id | |||||
self.name = name | |||||
self.args = args | |||||
self.kwargs = kwargs | |||||
self.delivery_info = delivery_info | |||||
self.hostname = hostname | |||||
@classmethod | |||||
def from_request(cls, request): | |||||
return cls(request.id, request.name, request.args, | |||||
request.kwargs, request.delivery_info, request.hostname) | |||||
class Batches(Task): | |||||
abstract = True | |||||
#: Maximum number of message in buffer. | |||||
flush_every = 10 | |||||
#: Timeout in seconds before buffer is flushed anyway. | |||||
flush_interval = 30 | |||||
def __init__(self): | |||||
self._buffer = Queue() | |||||
self._count = count(1) | |||||
self._tref = None | |||||
self._pool = None | |||||
def run(self, requests): | |||||
raise NotImplementedError('must implement run(requests)') | |||||
def Strategy(self, task, app, consumer): | |||||
self._pool = consumer.pool | |||||
hostname = consumer.hostname | |||||
eventer = consumer.event_dispatcher | |||||
Req = Request | |||||
connection_errors = consumer.connection_errors | |||||
timer = consumer.timer | |||||
put_buffer = self._buffer.put | |||||
flush_buffer = self._do_flush | |||||
def task_message_handler(message, body, ack, reject, callbacks, **kw): | |||||
request = Req(body, on_ack=ack, app=app, hostname=hostname, | |||||
events=eventer, task=task, | |||||
connection_errors=connection_errors, | |||||
delivery_info=message.delivery_info) | |||||
put_buffer(request) | |||||
if self._tref is None: # first request starts flush timer. | |||||
self._tref = timer.call_repeatedly( | |||||
self.flush_interval, flush_buffer, | |||||
) | |||||
if not next(self._count) % self.flush_every: | |||||
flush_buffer() | |||||
return task_message_handler | |||||
def flush(self, requests): | |||||
return self.apply_buffer(requests, ([SimpleRequest.from_request(r) | |||||
for r in requests], )) | |||||
def _do_flush(self): | |||||
logger.debug('Batches: Wake-up to flush buffer...') | |||||
requests = None | |||||
if self._buffer.qsize(): | |||||
requests = list(consume_queue(self._buffer)) | |||||
if requests: | |||||
logger.debug('Batches: Buffer complete: %s', len(requests)) | |||||
self.flush(requests) | |||||
if not requests: | |||||
logger.debug('Batches: Canceling timer: Nothing in buffer.') | |||||
if self._tref: | |||||
self._tref.cancel() # cancel timer. | |||||
self._tref = None | |||||
def apply_buffer(self, requests, args=(), kwargs={}): | |||||
acks_late = [], [] | |||||
[acks_late[r.task.acks_late].append(r) for r in requests] | |||||
assert requests and (acks_late[True] or acks_late[False]) | |||||
def on_accepted(pid, time_accepted): | |||||
[req.acknowledge() for req in acks_late[False]] | |||||
def on_return(result): | |||||
[req.acknowledge() for req in acks_late[True]] | |||||
return self._pool.apply_async( | |||||
apply_batches_task, | |||||
(self, args, 0, None), | |||||
accept_callback=on_accepted, | |||||
callback=acks_late[True] and on_return or noop, | |||||
) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.contrib.methods | |||||
====================== | |||||
Task decorator that supports creating tasks out of methods. | |||||
Examples | |||||
-------- | |||||
.. code-block:: python | |||||
from celery.contrib.methods import task | |||||
class X(object): | |||||
@task() | |||||
def add(self, x, y): | |||||
return x + y | |||||
or with any task decorator: | |||||
.. code-block:: python | |||||
from celery.contrib.methods import task_method | |||||
class X(object): | |||||
@app.task(filter=task_method) | |||||
def add(self, x, y): | |||||
return x + y | |||||
.. note:: | |||||
The task must use the new Task base class (:class:`celery.Task`), | |||||
and the old base class using classmethods (``celery.task.Task``, | |||||
``celery.task.base.Task``). | |||||
This means that you have to use the task decorator from a Celery app | |||||
instance, and not the old-API: | |||||
.. code-block:: python | |||||
from celery import task # BAD | |||||
from celery.task import task # ALSO BAD | |||||
# GOOD: | |||||
app = Celery(...) | |||||
@app.task(filter=task_method) | |||||
def foo(self): pass | |||||
# ALSO GOOD: | |||||
from celery import current_app | |||||
@current_app.task(filter=task_method) | |||||
def foo(self): pass | |||||
# ALSO GOOD: | |||||
from celery import shared_task | |||||
@shared_task(filter=task_method) | |||||
def foo(self): pass | |||||
Caveats | |||||
------- | |||||
- Automatic naming won't be able to know what the class name is. | |||||
The name will still be module_name + task_name, | |||||
so two methods with the same name in the same module will collide | |||||
so that only one task can run: | |||||
.. code-block:: python | |||||
class A(object): | |||||
@task() | |||||
def add(self, x, y): | |||||
return x + y | |||||
class B(object): | |||||
@task() | |||||
def add(self, x, y): | |||||
return x + y | |||||
would have to be written as: | |||||
.. code-block:: python | |||||
class A(object): | |||||
@task(name='A.add') | |||||
def add(self, x, y): | |||||
return x + y | |||||
class B(object): | |||||
@task(name='B.add') | |||||
def add(self, x, y): | |||||
return x + y | |||||
""" | |||||
from __future__ import absolute_import | |||||
from celery import current_app | |||||
__all__ = ['task_method', 'task'] | |||||
class task_method(object): | |||||
def __init__(self, task, *args, **kwargs): | |||||
self.task = task | |||||
def __get__(self, obj, type=None): | |||||
if obj is None: | |||||
return self.task | |||||
task = self.task.__class__() | |||||
task.__self__ = obj | |||||
return task | |||||
def task(*args, **kwargs): | |||||
return current_app.task(*args, **dict(kwargs, filter=task_method)) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.contrib.migrate | |||||
~~~~~~~~~~~~~~~~~~~~~~ | |||||
Migration tools. | |||||
""" | |||||
from __future__ import absolute_import, print_function, unicode_literals | |||||
import socket | |||||
from functools import partial | |||||
from itertools import cycle, islice | |||||
from kombu import eventloop, Queue | |||||
from kombu.common import maybe_declare | |||||
from kombu.utils.encoding import ensure_bytes | |||||
from celery.app import app_or_default | |||||
from celery.five import string, string_t | |||||
from celery.utils import worker_direct | |||||
__all__ = ['StopFiltering', 'State', 'republish', 'migrate_task', | |||||
'migrate_tasks', 'move', 'task_id_eq', 'task_id_in', | |||||
'start_filter', 'move_task_by_id', 'move_by_idmap', | |||||
'move_by_taskmap', 'move_direct', 'move_direct_by_id'] | |||||
MOVING_PROGRESS_FMT = """\ | |||||
Moving task {state.filtered}/{state.strtotal}: \ | |||||
{body[task]}[{body[id]}]\ | |||||
""" | |||||
class StopFiltering(Exception): | |||||
pass | |||||
class State(object): | |||||
count = 0 | |||||
filtered = 0 | |||||
total_apx = 0 | |||||
@property | |||||
def strtotal(self): | |||||
if not self.total_apx: | |||||
return '?' | |||||
return string(self.total_apx) | |||||
def __repr__(self): | |||||
if self.filtered: | |||||
return '^{0.filtered}'.format(self) | |||||
return '{0.count}/{0.strtotal}'.format(self) | |||||
def republish(producer, message, exchange=None, routing_key=None, | |||||
remove_props=['application_headers', | |||||
'content_type', | |||||
'content_encoding', | |||||
'headers']): | |||||
body = ensure_bytes(message.body) # use raw message body. | |||||
info, headers, props = (message.delivery_info, | |||||
message.headers, message.properties) | |||||
exchange = info['exchange'] if exchange is None else exchange | |||||
routing_key = info['routing_key'] if routing_key is None else routing_key | |||||
ctype, enc = message.content_type, message.content_encoding | |||||
# remove compression header, as this will be inserted again | |||||
# when the message is recompressed. | |||||
compression = headers.pop('compression', None) | |||||
for key in remove_props: | |||||
props.pop(key, None) | |||||
producer.publish(ensure_bytes(body), exchange=exchange, | |||||
routing_key=routing_key, compression=compression, | |||||
headers=headers, content_type=ctype, | |||||
content_encoding=enc, **props) | |||||
def migrate_task(producer, body_, message, queues=None): | |||||
info = message.delivery_info | |||||
queues = {} if queues is None else queues | |||||
republish(producer, message, | |||||
exchange=queues.get(info['exchange']), | |||||
routing_key=queues.get(info['routing_key'])) | |||||
def filter_callback(callback, tasks): | |||||
def filtered(body, message): | |||||
if tasks and body['task'] not in tasks: | |||||
return | |||||
return callback(body, message) | |||||
return filtered | |||||
def migrate_tasks(source, dest, migrate=migrate_task, app=None, | |||||
queues=None, **kwargs): | |||||
app = app_or_default(app) | |||||
queues = prepare_queues(queues) | |||||
producer = app.amqp.TaskProducer(dest) | |||||
migrate = partial(migrate, producer, queues=queues) | |||||
def on_declare_queue(queue): | |||||
new_queue = queue(producer.channel) | |||||
new_queue.name = queues.get(queue.name, queue.name) | |||||
if new_queue.routing_key == queue.name: | |||||
new_queue.routing_key = queues.get(queue.name, | |||||
new_queue.routing_key) | |||||
if new_queue.exchange.name == queue.name: | |||||
new_queue.exchange.name = queues.get(queue.name, queue.name) | |||||
new_queue.declare() | |||||
return start_filter(app, source, migrate, queues=queues, | |||||
on_declare_queue=on_declare_queue, **kwargs) | |||||
def _maybe_queue(app, q): | |||||
if isinstance(q, string_t): | |||||
return app.amqp.queues[q] | |||||
return q | |||||
def move(predicate, connection=None, exchange=None, routing_key=None, | |||||
source=None, app=None, callback=None, limit=None, transform=None, | |||||
**kwargs): | |||||
"""Find tasks by filtering them and move the tasks to a new queue. | |||||
:param predicate: Filter function used to decide which messages | |||||
to move. Must accept the standard signature of ``(body, message)`` | |||||
used by Kombu consumer callbacks. If the predicate wants the message | |||||
to be moved it must return either: | |||||
1) a tuple of ``(exchange, routing_key)``, or | |||||
2) a :class:`~kombu.entity.Queue` instance, or | |||||
3) any other true value which means the specified | |||||
``exchange`` and ``routing_key`` arguments will be used. | |||||
:keyword connection: Custom connection to use. | |||||
:keyword source: Optional list of source queues to use instead of the | |||||
default (which is the queues in :setting:`CELERY_QUEUES`). | |||||
This list can also contain new :class:`~kombu.entity.Queue` instances. | |||||
:keyword exchange: Default destination exchange. | |||||
:keyword routing_key: Default destination routing key. | |||||
:keyword limit: Limit number of messages to filter. | |||||
:keyword callback: Callback called after message moved, | |||||
with signature ``(state, body, message)``. | |||||
:keyword transform: Optional function to transform the return | |||||
value (destination) of the filter function. | |||||
Also supports the same keyword arguments as :func:`start_filter`. | |||||
To demonstrate, the :func:`move_task_by_id` operation can be implemented | |||||
like this: | |||||
.. code-block:: python | |||||
def is_wanted_task(body, message): | |||||
if body['id'] == wanted_id: | |||||
return Queue('foo', exchange=Exchange('foo'), | |||||
routing_key='foo') | |||||
move(is_wanted_task) | |||||
or with a transform: | |||||
.. code-block:: python | |||||
def transform(value): | |||||
if isinstance(value, string_t): | |||||
return Queue(value, Exchange(value), value) | |||||
return value | |||||
move(is_wanted_task, transform=transform) | |||||
The predicate may also return a tuple of ``(exchange, routing_key)`` | |||||
to specify the destination to where the task should be moved, | |||||
or a :class:`~kombu.entitiy.Queue` instance. | |||||
Any other true value means that the task will be moved to the | |||||
default exchange/routing_key. | |||||
""" | |||||
app = app_or_default(app) | |||||
queues = [_maybe_queue(app, queue) for queue in source or []] or None | |||||
with app.connection_or_acquire(connection, pool=False) as conn: | |||||
producer = app.amqp.TaskProducer(conn) | |||||
state = State() | |||||
def on_task(body, message): | |||||
ret = predicate(body, message) | |||||
if ret: | |||||
if transform: | |||||
ret = transform(ret) | |||||
if isinstance(ret, Queue): | |||||
maybe_declare(ret, conn.default_channel) | |||||
ex, rk = ret.exchange.name, ret.routing_key | |||||
else: | |||||
ex, rk = expand_dest(ret, exchange, routing_key) | |||||
republish(producer, message, | |||||
exchange=ex, routing_key=rk) | |||||
message.ack() | |||||
state.filtered += 1 | |||||
if callback: | |||||
callback(state, body, message) | |||||
if limit and state.filtered >= limit: | |||||
raise StopFiltering() | |||||
return start_filter(app, conn, on_task, consume_from=queues, **kwargs) | |||||
def expand_dest(ret, exchange, routing_key): | |||||
try: | |||||
ex, rk = ret | |||||
except (TypeError, ValueError): | |||||
ex, rk = exchange, routing_key | |||||
return ex, rk | |||||
def task_id_eq(task_id, body, message): | |||||
return body['id'] == task_id | |||||
def task_id_in(ids, body, message): | |||||
return body['id'] in ids | |||||
def prepare_queues(queues): | |||||
if isinstance(queues, string_t): | |||||
queues = queues.split(',') | |||||
if isinstance(queues, list): | |||||
queues = dict(tuple(islice(cycle(q.split(':')), None, 2)) | |||||
for q in queues) | |||||
if queues is None: | |||||
queues = {} | |||||
return queues | |||||
def start_filter(app, conn, filter, limit=None, timeout=1.0, | |||||
ack_messages=False, tasks=None, queues=None, | |||||
callback=None, forever=False, on_declare_queue=None, | |||||
consume_from=None, state=None, accept=None, **kwargs): | |||||
state = state or State() | |||||
queues = prepare_queues(queues) | |||||
consume_from = [_maybe_queue(app, q) | |||||
for q in consume_from or list(queues)] | |||||
if isinstance(tasks, string_t): | |||||
tasks = set(tasks.split(',')) | |||||
if tasks is None: | |||||
tasks = set([]) | |||||
def update_state(body, message): | |||||
state.count += 1 | |||||
if limit and state.count >= limit: | |||||
raise StopFiltering() | |||||
def ack_message(body, message): | |||||
message.ack() | |||||
consumer = app.amqp.TaskConsumer(conn, queues=consume_from, accept=accept) | |||||
if tasks: | |||||
filter = filter_callback(filter, tasks) | |||||
update_state = filter_callback(update_state, tasks) | |||||
ack_message = filter_callback(ack_message, tasks) | |||||
consumer.register_callback(filter) | |||||
consumer.register_callback(update_state) | |||||
if ack_messages: | |||||
consumer.register_callback(ack_message) | |||||
if callback is not None: | |||||
callback = partial(callback, state) | |||||
if tasks: | |||||
callback = filter_callback(callback, tasks) | |||||
consumer.register_callback(callback) | |||||
# declare all queues on the new broker. | |||||
for queue in consumer.queues: | |||||
if queues and queue.name not in queues: | |||||
continue | |||||
if on_declare_queue is not None: | |||||
on_declare_queue(queue) | |||||
try: | |||||
_, mcount, _ = queue(consumer.channel).queue_declare(passive=True) | |||||
if mcount: | |||||
state.total_apx += mcount | |||||
except conn.channel_errors: | |||||
pass | |||||
# start migrating messages. | |||||
with consumer: | |||||
try: | |||||
for _ in eventloop(conn, # pragma: no cover | |||||
timeout=timeout, ignore_timeouts=forever): | |||||
pass | |||||
except socket.timeout: | |||||
pass | |||||
except StopFiltering: | |||||
pass | |||||
return state | |||||
def move_task_by_id(task_id, dest, **kwargs): | |||||
"""Find a task by id and move it to another queue. | |||||
:param task_id: Id of task to move. | |||||
:param dest: Destination queue. | |||||
Also supports the same keyword arguments as :func:`move`. | |||||
""" | |||||
return move_by_idmap({task_id: dest}, **kwargs) | |||||
def move_by_idmap(map, **kwargs): | |||||
"""Moves tasks by matching from a ``task_id: queue`` mapping, | |||||
where ``queue`` is a queue to move the task to. | |||||
Example:: | |||||
>>> move_by_idmap({ | |||||
... '5bee6e82-f4ac-468e-bd3d-13e8600250bc': Queue('name'), | |||||
... 'ada8652d-aef3-466b-abd2-becdaf1b82b3': Queue('name'), | |||||
... '3a2b140d-7db1-41ba-ac90-c36a0ef4ab1f': Queue('name')}, | |||||
... queues=['hipri']) | |||||
""" | |||||
def task_id_in_map(body, message): | |||||
return map.get(body['id']) | |||||
# adding the limit means that we don't have to consume any more | |||||
# when we've found everything. | |||||
return move(task_id_in_map, limit=len(map), **kwargs) | |||||
def move_by_taskmap(map, **kwargs): | |||||
"""Moves tasks by matching from a ``task_name: queue`` mapping, | |||||
where ``queue`` is the queue to move the task to. | |||||
Example:: | |||||
>>> move_by_taskmap({ | |||||
... 'tasks.add': Queue('name'), | |||||
... 'tasks.mul': Queue('name'), | |||||
... }) | |||||
""" | |||||
def task_name_in_map(body, message): | |||||
return map.get(body['task']) # <- name of task | |||||
return move(task_name_in_map, **kwargs) | |||||
def filter_status(state, body, message, **kwargs): | |||||
print(MOVING_PROGRESS_FMT.format(state=state, body=body, **kwargs)) | |||||
move_direct = partial(move, transform=worker_direct) | |||||
move_direct_by_id = partial(move_task_by_id, transform=worker_direct) | |||||
move_direct_by_idmap = partial(move_by_idmap, transform=worker_direct) | |||||
move_direct_by_taskmap = partial(move_by_taskmap, transform=worker_direct) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.contrib.rdb | |||||
================== | |||||
Remote debugger for Celery tasks running in multiprocessing pool workers. | |||||
Inspired by http://snippets.dzone.com/posts/show/7248 | |||||
**Usage** | |||||
.. code-block:: python | |||||
from celery.contrib import rdb | |||||
from celery import task | |||||
@task() | |||||
def add(x, y): | |||||
result = x + y | |||||
rdb.set_trace() | |||||
return result | |||||
**Environment Variables** | |||||
.. envvar:: CELERY_RDB_HOST | |||||
Hostname to bind to. Default is '127.0.01', which means the socket | |||||
will only be accessible from the local host. | |||||
.. envvar:: CELERY_RDB_PORT | |||||
Base port to bind to. Default is 6899. | |||||
The debugger will try to find an available port starting from the | |||||
base port. The selected port will be logged by the worker. | |||||
""" | |||||
from __future__ import absolute_import, print_function | |||||
import errno | |||||
import os | |||||
import socket | |||||
import sys | |||||
from pdb import Pdb | |||||
from billiard import current_process | |||||
from celery.five import range | |||||
__all__ = ['CELERY_RDB_HOST', 'CELERY_RDB_PORT', 'default_port', | |||||
'Rdb', 'debugger', 'set_trace'] | |||||
default_port = 6899 | |||||
CELERY_RDB_HOST = os.environ.get('CELERY_RDB_HOST') or '127.0.0.1' | |||||
CELERY_RDB_PORT = int(os.environ.get('CELERY_RDB_PORT') or default_port) | |||||
#: Holds the currently active debugger. | |||||
_current = [None] | |||||
_frame = getattr(sys, '_getframe') | |||||
NO_AVAILABLE_PORT = """\ | |||||
{self.ident}: Couldn't find an available port. | |||||
Please specify one using the CELERY_RDB_PORT environment variable. | |||||
""" | |||||
BANNER = """\ | |||||
{self.ident}: Please telnet into {self.host} {self.port}. | |||||
Type `exit` in session to continue. | |||||
{self.ident}: Waiting for client... | |||||
""" | |||||
SESSION_STARTED = '{self.ident}: Now in session with {self.remote_addr}.' | |||||
SESSION_ENDED = '{self.ident}: Session with {self.remote_addr} ended.' | |||||
class Rdb(Pdb): | |||||
me = 'Remote Debugger' | |||||
_prev_outs = None | |||||
_sock = None | |||||
def __init__(self, host=CELERY_RDB_HOST, port=CELERY_RDB_PORT, | |||||
port_search_limit=100, port_skew=+0, out=sys.stdout): | |||||
self.active = True | |||||
self.out = out | |||||
self._prev_handles = sys.stdin, sys.stdout | |||||
self._sock, this_port = self.get_avail_port( | |||||
host, port, port_search_limit, port_skew, | |||||
) | |||||
self._sock.setblocking(1) | |||||
self._sock.listen(1) | |||||
self.ident = '{0}:{1}'.format(self.me, this_port) | |||||
self.host = host | |||||
self.port = this_port | |||||
self.say(BANNER.format(self=self)) | |||||
self._client, address = self._sock.accept() | |||||
self._client.setblocking(1) | |||||
self.remote_addr = ':'.join(str(v) for v in address) | |||||
self.say(SESSION_STARTED.format(self=self)) | |||||
self._handle = sys.stdin = sys.stdout = self._client.makefile('rw') | |||||
Pdb.__init__(self, completekey='tab', | |||||
stdin=self._handle, stdout=self._handle) | |||||
def get_avail_port(self, host, port, search_limit=100, skew=+0): | |||||
try: | |||||
_, skew = current_process().name.split('-') | |||||
skew = int(skew) | |||||
except ValueError: | |||||
pass | |||||
this_port = None | |||||
for i in range(search_limit): | |||||
_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||||
this_port = port + skew + i | |||||
try: | |||||
_sock.bind((host, this_port)) | |||||
except socket.error as exc: | |||||
if exc.errno in [errno.EADDRINUSE, errno.EINVAL]: | |||||
continue | |||||
raise | |||||
else: | |||||
return _sock, this_port | |||||
else: | |||||
raise Exception(NO_AVAILABLE_PORT.format(self=self)) | |||||
def say(self, m): | |||||
print(m, file=self.out) | |||||
def __enter__(self): | |||||
return self | |||||
def __exit__(self, *exc_info): | |||||
self._close_session() | |||||
def _close_session(self): | |||||
self.stdin, self.stdout = sys.stdin, sys.stdout = self._prev_handles | |||||
if self.active: | |||||
if self._handle is not None: | |||||
self._handle.close() | |||||
if self._client is not None: | |||||
self._client.close() | |||||
if self._sock is not None: | |||||
self._sock.close() | |||||
self.active = False | |||||
self.say(SESSION_ENDED.format(self=self)) | |||||
def do_continue(self, arg): | |||||
self._close_session() | |||||
self.set_continue() | |||||
return 1 | |||||
do_c = do_cont = do_continue | |||||
def do_quit(self, arg): | |||||
self._close_session() | |||||
self.set_quit() | |||||
return 1 | |||||
do_q = do_exit = do_quit | |||||
def set_quit(self): | |||||
# this raises a BdbQuit exception that we are unable to catch. | |||||
sys.settrace(None) | |||||
def debugger(): | |||||
"""Return the current debugger instance (if any), | |||||
or creates a new one.""" | |||||
rdb = _current[0] | |||||
if rdb is None or not rdb.active: | |||||
rdb = _current[0] = Rdb() | |||||
return rdb | |||||
def set_trace(frame=None): | |||||
"""Set breakpoint at current location, or a specified frame""" | |||||
if frame is None: | |||||
frame = _frame().f_back | |||||
return debugger().set_trace(frame) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.contrib.sphinx | |||||
===================== | |||||
Sphinx documentation plugin | |||||
**Usage** | |||||
Add the extension to your :file:`docs/conf.py` configuration module: | |||||
.. code-block:: python | |||||
extensions = (..., | |||||
'celery.contrib.sphinx') | |||||
If you would like to change the prefix for tasks in reference documentation | |||||
then you can change the ``celery_task_prefix`` configuration value: | |||||
.. code-block:: python | |||||
celery_task_prefix = '(task)' # < default | |||||
With the extension installed `autodoc` will automatically find | |||||
task decorated objects and generate the correct (as well as | |||||
add a ``(task)`` prefix), and you can also refer to the tasks | |||||
using `:task:proj.tasks.add` syntax. | |||||
Use ``.. autotask::`` to manually document a task. | |||||
""" | |||||
from __future__ import absolute_import | |||||
try: | |||||
from inspect import formatargspec, getfullargspec as getargspec | |||||
except ImportError: # Py2 | |||||
from inspect import formatargspec, getargspec # noqa | |||||
from sphinx.domains.python import PyModulelevel | |||||
from sphinx.ext.autodoc import FunctionDocumenter | |||||
from celery.app.task import BaseTask | |||||
class TaskDocumenter(FunctionDocumenter): | |||||
objtype = 'task' | |||||
member_order = 11 | |||||
@classmethod | |||||
def can_document_member(cls, member, membername, isattr, parent): | |||||
return isinstance(member, BaseTask) and getattr(member, '__wrapped__') | |||||
def format_args(self): | |||||
wrapped = getattr(self.object, '__wrapped__') | |||||
if wrapped is not None: | |||||
argspec = getargspec(wrapped) | |||||
fmt = formatargspec(*argspec) | |||||
fmt = fmt.replace('\\', '\\\\') | |||||
return fmt | |||||
return '' | |||||
def document_members(self, all_members=False): | |||||
pass | |||||
class TaskDirective(PyModulelevel): | |||||
def get_signature_prefix(self, sig): | |||||
return self.env.config.celery_task_prefix | |||||
def setup(app): | |||||
app.add_autodocumenter(TaskDocumenter) | |||||
app.domains['py'].directives['task'] = TaskDirective | |||||
app.add_config_value('celery_task_prefix', '(task)', True) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.datastructures | |||||
~~~~~~~~~~~~~~~~~~~~~ | |||||
Custom types and data structures. | |||||
""" | |||||
from __future__ import absolute_import, print_function, unicode_literals | |||||
import sys | |||||
import time | |||||
from collections import defaultdict, Mapping, MutableMapping, MutableSet | |||||
from heapq import heapify, heappush, heappop | |||||
from functools import partial | |||||
from itertools import chain | |||||
from billiard.einfo import ExceptionInfo # noqa | |||||
from kombu.utils.encoding import safe_str | |||||
from kombu.utils.limits import TokenBucket # noqa | |||||
from celery.five import items | |||||
from celery.utils.functional import LRUCache, first, uniq # noqa | |||||
try: | |||||
from django.utils.functional import LazyObject, LazySettings | |||||
except ImportError: | |||||
class LazyObject(object): # noqa | |||||
pass | |||||
LazySettings = LazyObject # noqa | |||||
DOT_HEAD = """ | |||||
{IN}{type} {id} {{ | |||||
{INp}graph [{attrs}] | |||||
""" | |||||
DOT_ATTR = '{name}={value}' | |||||
DOT_NODE = '{INp}"{0}" [{attrs}]' | |||||
DOT_EDGE = '{INp}"{0}" {dir} "{1}" [{attrs}]' | |||||
DOT_ATTRSEP = ', ' | |||||
DOT_DIRS = {'graph': '--', 'digraph': '->'} | |||||
DOT_TAIL = '{IN}}}' | |||||
__all__ = ['GraphFormatter', 'CycleError', 'DependencyGraph', | |||||
'AttributeDictMixin', 'AttributeDict', 'DictAttribute', | |||||
'ConfigurationView', 'LimitedSet'] | |||||
def force_mapping(m): | |||||
if isinstance(m, (LazyObject, LazySettings)): | |||||
m = m._wrapped | |||||
return DictAttribute(m) if not isinstance(m, Mapping) else m | |||||
class GraphFormatter(object): | |||||
_attr = DOT_ATTR.strip() | |||||
_node = DOT_NODE.strip() | |||||
_edge = DOT_EDGE.strip() | |||||
_head = DOT_HEAD.strip() | |||||
_tail = DOT_TAIL.strip() | |||||
_attrsep = DOT_ATTRSEP | |||||
_dirs = dict(DOT_DIRS) | |||||
scheme = { | |||||
'shape': 'box', | |||||
'arrowhead': 'vee', | |||||
'style': 'filled', | |||||
'fontname': 'HelveticaNeue', | |||||
} | |||||
edge_scheme = { | |||||
'color': 'darkseagreen4', | |||||
'arrowcolor': 'black', | |||||
'arrowsize': 0.7, | |||||
} | |||||
node_scheme = {'fillcolor': 'palegreen3', 'color': 'palegreen4'} | |||||
term_scheme = {'fillcolor': 'palegreen1', 'color': 'palegreen2'} | |||||
graph_scheme = {'bgcolor': 'mintcream'} | |||||
def __init__(self, root=None, type=None, id=None, | |||||
indent=0, inw=' ' * 4, **scheme): | |||||
self.id = id or 'dependencies' | |||||
self.root = root | |||||
self.type = type or 'digraph' | |||||
self.direction = self._dirs[self.type] | |||||
self.IN = inw * (indent or 0) | |||||
self.INp = self.IN + inw | |||||
self.scheme = dict(self.scheme, **scheme) | |||||
self.graph_scheme = dict(self.graph_scheme, root=self.label(self.root)) | |||||
def attr(self, name, value): | |||||
value = '"{0}"'.format(value) | |||||
return self.FMT(self._attr, name=name, value=value) | |||||
def attrs(self, d, scheme=None): | |||||
d = dict(self.scheme, **dict(scheme, **d or {}) if scheme else d) | |||||
return self._attrsep.join( | |||||
safe_str(self.attr(k, v)) for k, v in items(d) | |||||
) | |||||
def head(self, **attrs): | |||||
return self.FMT( | |||||
self._head, id=self.id, type=self.type, | |||||
attrs=self.attrs(attrs, self.graph_scheme), | |||||
) | |||||
def tail(self): | |||||
return self.FMT(self._tail) | |||||
def label(self, obj): | |||||
return obj | |||||
def node(self, obj, **attrs): | |||||
return self.draw_node(obj, self.node_scheme, attrs) | |||||
def terminal_node(self, obj, **attrs): | |||||
return self.draw_node(obj, self.term_scheme, attrs) | |||||
def edge(self, a, b, **attrs): | |||||
return self.draw_edge(a, b, **attrs) | |||||
def _enc(self, s): | |||||
return s.encode('utf-8', 'ignore') | |||||
def FMT(self, fmt, *args, **kwargs): | |||||
return self._enc(fmt.format( | |||||
*args, **dict(kwargs, IN=self.IN, INp=self.INp) | |||||
)) | |||||
def draw_edge(self, a, b, scheme=None, attrs=None): | |||||
return self.FMT( | |||||
self._edge, self.label(a), self.label(b), | |||||
dir=self.direction, attrs=self.attrs(attrs, self.edge_scheme), | |||||
) | |||||
def draw_node(self, obj, scheme=None, attrs=None): | |||||
return self.FMT( | |||||
self._node, self.label(obj), attrs=self.attrs(attrs, scheme), | |||||
) | |||||
class CycleError(Exception): | |||||
"""A cycle was detected in an acyclic graph.""" | |||||
class DependencyGraph(object): | |||||
"""A directed acyclic graph of objects and their dependencies. | |||||
Supports a robust topological sort | |||||
to detect the order in which they must be handled. | |||||
Takes an optional iterator of ``(obj, dependencies)`` | |||||
tuples to build the graph from. | |||||
.. warning:: | |||||
Does not support cycle detection. | |||||
""" | |||||
def __init__(self, it=None, formatter=None): | |||||
self.formatter = formatter or GraphFormatter() | |||||
self.adjacent = {} | |||||
if it is not None: | |||||
self.update(it) | |||||
def add_arc(self, obj): | |||||
"""Add an object to the graph.""" | |||||
self.adjacent.setdefault(obj, []) | |||||
def add_edge(self, A, B): | |||||
"""Add an edge from object ``A`` to object ``B`` | |||||
(``A`` depends on ``B``).""" | |||||
self[A].append(B) | |||||
def connect(self, graph): | |||||
"""Add nodes from another graph.""" | |||||
self.adjacent.update(graph.adjacent) | |||||
def topsort(self): | |||||
"""Sort the graph topologically. | |||||
:returns: a list of objects in the order | |||||
in which they must be handled. | |||||
""" | |||||
graph = DependencyGraph() | |||||
components = self._tarjan72() | |||||
NC = dict((node, component) | |||||
for component in components | |||||
for node in component) | |||||
for component in components: | |||||
graph.add_arc(component) | |||||
for node in self: | |||||
node_c = NC[node] | |||||
for successor in self[node]: | |||||
successor_c = NC[successor] | |||||
if node_c != successor_c: | |||||
graph.add_edge(node_c, successor_c) | |||||
return [t[0] for t in graph._khan62()] | |||||
def valency_of(self, obj): | |||||
"""Return the valency (degree) of a vertex in the graph.""" | |||||
try: | |||||
l = [len(self[obj])] | |||||
except KeyError: | |||||
return 0 | |||||
for node in self[obj]: | |||||
l.append(self.valency_of(node)) | |||||
return sum(l) | |||||
def update(self, it): | |||||
"""Update the graph with data from a list | |||||
of ``(obj, dependencies)`` tuples.""" | |||||
tups = list(it) | |||||
for obj, _ in tups: | |||||
self.add_arc(obj) | |||||
for obj, deps in tups: | |||||
for dep in deps: | |||||
self.add_edge(obj, dep) | |||||
def edges(self): | |||||
"""Return generator that yields for all edges in the graph.""" | |||||
return (obj for obj, adj in items(self) if adj) | |||||
def _khan62(self): | |||||
"""Khans simple topological sort algorithm from '62 | |||||
See http://en.wikipedia.org/wiki/Topological_sorting | |||||
""" | |||||
count = defaultdict(lambda: 0) | |||||
result = [] | |||||
for node in self: | |||||
for successor in self[node]: | |||||
count[successor] += 1 | |||||
ready = [node for node in self if not count[node]] | |||||
while ready: | |||||
node = ready.pop() | |||||
result.append(node) | |||||
for successor in self[node]: | |||||
count[successor] -= 1 | |||||
if count[successor] == 0: | |||||
ready.append(successor) | |||||
result.reverse() | |||||
return result | |||||
def _tarjan72(self): | |||||
"""Tarjan's algorithm to find strongly connected components. | |||||
See http://bit.ly/vIMv3h. | |||||
""" | |||||
result, stack, low = [], [], {} | |||||
def visit(node): | |||||
if node in low: | |||||
return | |||||
num = len(low) | |||||
low[node] = num | |||||
stack_pos = len(stack) | |||||
stack.append(node) | |||||
for successor in self[node]: | |||||
visit(successor) | |||||
low[node] = min(low[node], low[successor]) | |||||
if num == low[node]: | |||||
component = tuple(stack[stack_pos:]) | |||||
stack[stack_pos:] = [] | |||||
result.append(component) | |||||
for item in component: | |||||
low[item] = len(self) | |||||
for node in self: | |||||
visit(node) | |||||
return result | |||||
def to_dot(self, fh, formatter=None): | |||||
"""Convert the graph to DOT format. | |||||
:param fh: A file, or a file-like object to write the graph to. | |||||
""" | |||||
seen = set() | |||||
draw = formatter or self.formatter | |||||
P = partial(print, file=fh) | |||||
def if_not_seen(fun, obj): | |||||
if draw.label(obj) not in seen: | |||||
P(fun(obj)) | |||||
seen.add(draw.label(obj)) | |||||
P(draw.head()) | |||||
for obj, adjacent in items(self): | |||||
if not adjacent: | |||||
if_not_seen(draw.terminal_node, obj) | |||||
for req in adjacent: | |||||
if_not_seen(draw.node, obj) | |||||
P(draw.edge(obj, req)) | |||||
P(draw.tail()) | |||||
def format(self, obj): | |||||
return self.formatter(obj) if self.formatter else obj | |||||
def __iter__(self): | |||||
return iter(self.adjacent) | |||||
def __getitem__(self, node): | |||||
return self.adjacent[node] | |||||
def __len__(self): | |||||
return len(self.adjacent) | |||||
def __contains__(self, obj): | |||||
return obj in self.adjacent | |||||
def _iterate_items(self): | |||||
return items(self.adjacent) | |||||
items = iteritems = _iterate_items | |||||
def __repr__(self): | |||||
return '\n'.join(self.repr_node(N) for N in self) | |||||
def repr_node(self, obj, level=1, fmt='{0}({1})'): | |||||
output = [fmt.format(obj, self.valency_of(obj))] | |||||
if obj in self: | |||||
for other in self[obj]: | |||||
d = fmt.format(other, self.valency_of(other)) | |||||
output.append(' ' * level + d) | |||||
output.extend(self.repr_node(other, level + 1).split('\n')[1:]) | |||||
return '\n'.join(output) | |||||
class AttributeDictMixin(object): | |||||
"""Augment classes with a Mapping interface by adding attribute access. | |||||
I.e. `d.key -> d[key]`. | |||||
""" | |||||
def __getattr__(self, k): | |||||
"""`d.key -> d[key]`""" | |||||
try: | |||||
return self[k] | |||||
except KeyError: | |||||
raise AttributeError( | |||||
'{0!r} object has no attribute {1!r}'.format( | |||||
type(self).__name__, k)) | |||||
def __setattr__(self, key, value): | |||||
"""`d[key] = value -> d.key = value`""" | |||||
self[key] = value | |||||
class AttributeDict(dict, AttributeDictMixin): | |||||
"""Dict subclass with attribute access.""" | |||||
pass | |||||
class DictAttribute(object): | |||||
"""Dict interface to attributes. | |||||
`obj[k] -> obj.k` | |||||
`obj[k] = val -> obj.k = val` | |||||
""" | |||||
obj = None | |||||
def __init__(self, obj): | |||||
object.__setattr__(self, 'obj', obj) | |||||
def __getattr__(self, key): | |||||
return getattr(self.obj, key) | |||||
def __setattr__(self, key, value): | |||||
return setattr(self.obj, key, value) | |||||
def get(self, key, default=None): | |||||
try: | |||||
return self[key] | |||||
except KeyError: | |||||
return default | |||||
def setdefault(self, key, default): | |||||
try: | |||||
return self[key] | |||||
except KeyError: | |||||
self[key] = default | |||||
return default | |||||
def __getitem__(self, key): | |||||
try: | |||||
return getattr(self.obj, key) | |||||
except AttributeError: | |||||
raise KeyError(key) | |||||
def __setitem__(self, key, value): | |||||
setattr(self.obj, key, value) | |||||
def __contains__(self, key): | |||||
return hasattr(self.obj, key) | |||||
def _iterate_keys(self): | |||||
return iter(dir(self.obj)) | |||||
iterkeys = _iterate_keys | |||||
def __iter__(self): | |||||
return self._iterate_keys() | |||||
def _iterate_items(self): | |||||
for key in self._iterate_keys(): | |||||
yield key, getattr(self.obj, key) | |||||
iteritems = _iterate_items | |||||
def _iterate_values(self): | |||||
for key in self._iterate_keys(): | |||||
yield getattr(self.obj, key) | |||||
itervalues = _iterate_values | |||||
if sys.version_info[0] == 3: # pragma: no cover | |||||
items = _iterate_items | |||||
keys = _iterate_keys | |||||
values = _iterate_values | |||||
else: | |||||
def keys(self): | |||||
return list(self) | |||||
def items(self): | |||||
return list(self._iterate_items()) | |||||
def values(self): | |||||
return list(self._iterate_values()) | |||||
MutableMapping.register(DictAttribute) | |||||
class ConfigurationView(AttributeDictMixin): | |||||
"""A view over an applications configuration dicts. | |||||
Custom (but older) version of :class:`collections.ChainMap`. | |||||
If the key does not exist in ``changes``, the ``defaults`` dicts | |||||
are consulted. | |||||
:param changes: Dict containing changes to the configuration. | |||||
:param defaults: List of dicts containing the default configuration. | |||||
""" | |||||
changes = None | |||||
defaults = None | |||||
_order = None | |||||
def __init__(self, changes, defaults): | |||||
self.__dict__.update(changes=changes, defaults=defaults, | |||||
_order=[changes] + defaults) | |||||
def add_defaults(self, d): | |||||
d = force_mapping(d) | |||||
self.defaults.insert(0, d) | |||||
self._order.insert(1, d) | |||||
def __getitem__(self, key): | |||||
for d in self._order: | |||||
try: | |||||
return d[key] | |||||
except KeyError: | |||||
pass | |||||
raise KeyError(key) | |||||
def __setitem__(self, key, value): | |||||
self.changes[key] = value | |||||
def first(self, *keys): | |||||
return first(None, (self.get(key) for key in keys)) | |||||
def get(self, key, default=None): | |||||
try: | |||||
return self[key] | |||||
except KeyError: | |||||
return default | |||||
def clear(self): | |||||
"""Remove all changes, but keep defaults.""" | |||||
self.changes.clear() | |||||
def setdefault(self, key, default): | |||||
try: | |||||
return self[key] | |||||
except KeyError: | |||||
self[key] = default | |||||
return default | |||||
def update(self, *args, **kwargs): | |||||
return self.changes.update(*args, **kwargs) | |||||
def __contains__(self, key): | |||||
return any(key in m for m in self._order) | |||||
def __bool__(self): | |||||
return any(self._order) | |||||
__nonzero__ = __bool__ # Py2 | |||||
def __repr__(self): | |||||
return repr(dict(items(self))) | |||||
def __iter__(self): | |||||
return self._iterate_keys() | |||||
def __len__(self): | |||||
# The logic for iterating keys includes uniq(), | |||||
# so to be safe we count by explicitly iterating | |||||
return len(set().union(*self._order)) | |||||
def _iter(self, op): | |||||
# defaults must be first in the stream, so values in | |||||
# changes takes precedence. | |||||
return chain(*[op(d) for d in reversed(self._order)]) | |||||
def _iterate_keys(self): | |||||
return uniq(self._iter(lambda d: d)) | |||||
iterkeys = _iterate_keys | |||||
def _iterate_items(self): | |||||
return ((key, self[key]) for key in self) | |||||
iteritems = _iterate_items | |||||
def _iterate_values(self): | |||||
return (self[key] for key in self) | |||||
itervalues = _iterate_values | |||||
if sys.version_info[0] == 3: # pragma: no cover | |||||
keys = _iterate_keys | |||||
items = _iterate_items | |||||
values = _iterate_values | |||||
else: # noqa | |||||
def keys(self): | |||||
return list(self._iterate_keys()) | |||||
def items(self): | |||||
return list(self._iterate_items()) | |||||
def values(self): | |||||
return list(self._iterate_values()) | |||||
MutableMapping.register(ConfigurationView) | |||||
class LimitedSet(object): | |||||
"""Kind-of Set with limitations. | |||||
Good for when you need to test for membership (`a in set`), | |||||
but the set should not grow unbounded. | |||||
:keyword maxlen: Maximum number of members before we start | |||||
evicting expired members. | |||||
:keyword expires: Time in seconds, before a membership expires. | |||||
""" | |||||
def __init__(self, maxlen=None, expires=None, data=None, heap=None): | |||||
# heap is ignored | |||||
self.maxlen = maxlen | |||||
self.expires = expires | |||||
self._data = {} if data is None else data | |||||
self._heap = [] | |||||
# make shortcuts | |||||
self.__len__ = self._heap.__len__ | |||||
self.__contains__ = self._data.__contains__ | |||||
self._refresh_heap() | |||||
def _refresh_heap(self): | |||||
self._heap[:] = [(t, key) for key, t in items(self._data)] | |||||
heapify(self._heap) | |||||
def add(self, key, now=time.time, heappush=heappush): | |||||
"""Add a new member.""" | |||||
# offset is there to modify the length of the list, | |||||
# this way we can expire an item before inserting the value, | |||||
# and it will end up in the correct order. | |||||
self.purge(1, offset=1) | |||||
inserted = now() | |||||
self._data[key] = inserted | |||||
heappush(self._heap, (inserted, key)) | |||||
def clear(self): | |||||
"""Remove all members""" | |||||
self._data.clear() | |||||
self._heap[:] = [] | |||||
def discard(self, value): | |||||
"""Remove membership by finding value.""" | |||||
try: | |||||
itime = self._data[value] | |||||
except KeyError: | |||||
return | |||||
try: | |||||
self._heap.remove((itime, value)) | |||||
except ValueError: | |||||
pass | |||||
self._data.pop(value, None) | |||||
pop_value = discard # XXX compat | |||||
def purge(self, limit=None, offset=0, now=time.time): | |||||
"""Purge expired items.""" | |||||
H, maxlen = self._heap, self.maxlen | |||||
if not maxlen: | |||||
return | |||||
# If the data/heap gets corrupted and limit is None | |||||
# this will go into an infinite loop, so limit must | |||||
# have a value to guard the loop. | |||||
limit = len(self) + offset if limit is None else limit | |||||
i = 0 | |||||
while len(self) + offset > maxlen: | |||||
if i >= limit: | |||||
break | |||||
try: | |||||
item = heappop(H) | |||||
except IndexError: | |||||
break | |||||
if self.expires: | |||||
if now() < item[0] + self.expires: | |||||
heappush(H, item) | |||||
break | |||||
try: | |||||
self._data.pop(item[1]) | |||||
except KeyError: # out of sync with heap | |||||
pass | |||||
i += 1 | |||||
def update(self, other): | |||||
if isinstance(other, LimitedSet): | |||||
self._data.update(other._data) | |||||
self._refresh_heap() | |||||
else: | |||||
for obj in other: | |||||
self.add(obj) | |||||
def as_dict(self): | |||||
return self._data | |||||
def __eq__(self, other): | |||||
return self._heap == other._heap | |||||
def __ne__(self, other): | |||||
return not self.__eq__(other) | |||||
def __repr__(self): | |||||
return 'LimitedSet({0})'.format(len(self)) | |||||
def __iter__(self): | |||||
return (item[1] for item in self._heap) | |||||
def __len__(self): | |||||
return len(self._heap) | |||||
def __contains__(self, key): | |||||
return key in self._data | |||||
def __reduce__(self): | |||||
return self.__class__, (self.maxlen, self.expires, self._data) | |||||
MutableSet.register(LimitedSet) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.events | |||||
~~~~~~~~~~~~~ | |||||
Events is a stream of messages sent for certain actions occurring | |||||
in the worker (and clients if :setting:`CELERY_SEND_TASK_SENT_EVENT` | |||||
is enabled), used for monitoring purposes. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import os | |||||
import time | |||||
import threading | |||||
import warnings | |||||
from collections import deque | |||||
from contextlib import contextmanager | |||||
from copy import copy | |||||
from operator import itemgetter | |||||
from kombu import Exchange, Queue, Producer | |||||
from kombu.connection import maybe_channel | |||||
from kombu.mixins import ConsumerMixin | |||||
from kombu.utils import cached_property | |||||
from celery.app import app_or_default | |||||
from celery.utils import anon_nodename, uuid | |||||
from celery.utils.functional import dictfilter | |||||
from celery.utils.timeutils import adjust_timestamp, utcoffset, maybe_s_to_ms | |||||
__all__ = ['Events', 'Event', 'EventDispatcher', 'EventReceiver'] | |||||
event_exchange = Exchange('celeryev', type='topic') | |||||
_TZGETTER = itemgetter('utcoffset', 'timestamp') | |||||
W_YAJL = """ | |||||
anyjson is currently using the yajl library. | |||||
This json implementation is broken, it severely truncates floats | |||||
so timestamps will not work. | |||||
Please uninstall yajl or force anyjson to use a different library. | |||||
""" | |||||
CLIENT_CLOCK_SKEW = -1 | |||||
def get_exchange(conn): | |||||
ex = copy(event_exchange) | |||||
if conn.transport.driver_type == 'redis': | |||||
# quick hack for Issue #436 | |||||
ex.type = 'fanout' | |||||
return ex | |||||
def Event(type, _fields=None, __dict__=dict, __now__=time.time, **fields): | |||||
"""Create an event. | |||||
An event is a dictionary, the only required field is ``type``. | |||||
A ``timestamp`` field will be set to the current time if not provided. | |||||
""" | |||||
event = __dict__(_fields, **fields) if _fields else fields | |||||
if 'timestamp' not in event: | |||||
event.update(timestamp=__now__(), type=type) | |||||
else: | |||||
event['type'] = type | |||||
return event | |||||
def group_from(type): | |||||
"""Get the group part of an event type name. | |||||
E.g.:: | |||||
>>> group_from('task-sent') | |||||
'task' | |||||
>>> group_from('custom-my-event') | |||||
'custom' | |||||
""" | |||||
return type.split('-', 1)[0] | |||||
class EventDispatcher(object): | |||||
"""Dispatches event messages. | |||||
:param connection: Connection to the broker. | |||||
:keyword hostname: Hostname to identify ourselves as, | |||||
by default uses the hostname returned by | |||||
:func:`~celery.utils.anon_nodename`. | |||||
:keyword groups: List of groups to send events for. :meth:`send` will | |||||
ignore send requests to groups not in this list. | |||||
If this is :const:`None`, all events will be sent. Example groups | |||||
include ``"task"`` and ``"worker"``. | |||||
:keyword enabled: Set to :const:`False` to not actually publish any events, | |||||
making :meth:`send` a noop operation. | |||||
:keyword channel: Can be used instead of `connection` to specify | |||||
an exact channel to use when sending events. | |||||
:keyword buffer_while_offline: If enabled events will be buffered | |||||
while the connection is down. :meth:`flush` must be called | |||||
as soon as the connection is re-established. | |||||
You need to :meth:`close` this after use. | |||||
""" | |||||
DISABLED_TRANSPORTS = set(['sql']) | |||||
app = None | |||||
# set of callbacks to be called when :meth:`enabled`. | |||||
on_enabled = None | |||||
# set of callbacks to be called when :meth:`disabled`. | |||||
on_disabled = None | |||||
def __init__(self, connection=None, hostname=None, enabled=True, | |||||
channel=None, buffer_while_offline=True, app=None, | |||||
serializer=None, groups=None): | |||||
self.app = app_or_default(app or self.app) | |||||
self.connection = connection | |||||
self.channel = channel | |||||
self.hostname = hostname or anon_nodename() | |||||
self.buffer_while_offline = buffer_while_offline | |||||
self.mutex = threading.Lock() | |||||
self.producer = None | |||||
self._outbound_buffer = deque() | |||||
self.serializer = serializer or self.app.conf.CELERY_EVENT_SERIALIZER | |||||
self.on_enabled = set() | |||||
self.on_disabled = set() | |||||
self.groups = set(groups or []) | |||||
self.tzoffset = [-time.timezone, -time.altzone] | |||||
self.clock = self.app.clock | |||||
if not connection and channel: | |||||
self.connection = channel.connection.client | |||||
self.enabled = enabled | |||||
conninfo = self.connection or self.app.connection() | |||||
self.exchange = get_exchange(conninfo) | |||||
if conninfo.transport.driver_type in self.DISABLED_TRANSPORTS: | |||||
self.enabled = False | |||||
if self.enabled: | |||||
self.enable() | |||||
self.headers = {'hostname': self.hostname} | |||||
self.pid = os.getpid() | |||||
self.warn_if_yajl() | |||||
def warn_if_yajl(self): | |||||
import anyjson | |||||
if anyjson.implementation.name == 'yajl': | |||||
warnings.warn(UserWarning(W_YAJL)) | |||||
def __enter__(self): | |||||
return self | |||||
def __exit__(self, *exc_info): | |||||
self.close() | |||||
def enable(self): | |||||
self.producer = Producer(self.channel or self.connection, | |||||
exchange=self.exchange, | |||||
serializer=self.serializer) | |||||
self.enabled = True | |||||
for callback in self.on_enabled: | |||||
callback() | |||||
def disable(self): | |||||
if self.enabled: | |||||
self.enabled = False | |||||
self.close() | |||||
for callback in self.on_disabled: | |||||
callback() | |||||
def publish(self, type, fields, producer, retry=False, | |||||
retry_policy=None, blind=False, utcoffset=utcoffset, | |||||
Event=Event): | |||||
"""Publish event using a custom :class:`~kombu.Producer` | |||||
instance. | |||||
:param type: Event type name, with group separated by dash (`-`). | |||||
:param fields: Dictionary of event fields, must be json serializable. | |||||
:param producer: :class:`~kombu.Producer` instance to use, | |||||
only the ``publish`` method will be called. | |||||
:keyword retry: Retry in the event of connection failure. | |||||
:keyword retry_policy: Dict of custom retry policy, see | |||||
:meth:`~kombu.Connection.ensure`. | |||||
:keyword blind: Don't set logical clock value (also do not forward | |||||
the internal logical clock). | |||||
:keyword Event: Event type used to create event, | |||||
defaults to :func:`Event`. | |||||
:keyword utcoffset: Function returning the current utcoffset in hours. | |||||
""" | |||||
with self.mutex: | |||||
clock = None if blind else self.clock.forward() | |||||
event = Event(type, hostname=self.hostname, utcoffset=utcoffset(), | |||||
pid=self.pid, clock=clock, **fields) | |||||
exchange = self.exchange | |||||
producer.publish( | |||||
event, | |||||
routing_key=type.replace('-', '.'), | |||||
exchange=exchange.name, | |||||
retry=retry, | |||||
retry_policy=retry_policy, | |||||
declare=[exchange], | |||||
serializer=self.serializer, | |||||
headers=self.headers, | |||||
) | |||||
def send(self, type, blind=False, **fields): | |||||
"""Send event. | |||||
:param type: Event type name, with group separated by dash (`-`). | |||||
:keyword retry: Retry in the event of connection failure. | |||||
:keyword retry_policy: Dict of custom retry policy, see | |||||
:meth:`~kombu.Connection.ensure`. | |||||
:keyword blind: Don't set logical clock value (also do not forward | |||||
the internal logical clock). | |||||
:keyword Event: Event type used to create event, | |||||
defaults to :func:`Event`. | |||||
:keyword utcoffset: Function returning the current utcoffset in hours. | |||||
:keyword \*\*fields: Event fields, must be json serializable. | |||||
""" | |||||
if self.enabled: | |||||
groups = self.groups | |||||
if groups and group_from(type) not in groups: | |||||
return | |||||
try: | |||||
self.publish(type, fields, self.producer, blind) | |||||
except Exception as exc: | |||||
if not self.buffer_while_offline: | |||||
raise | |||||
self._outbound_buffer.append((type, fields, exc)) | |||||
def flush(self): | |||||
"""Flushes the outbound buffer.""" | |||||
while self._outbound_buffer: | |||||
try: | |||||
type, fields, _ = self._outbound_buffer.popleft() | |||||
except IndexError: | |||||
return | |||||
self.send(type, **fields) | |||||
def extend_buffer(self, other): | |||||
"""Copies the outbound buffer of another instance.""" | |||||
self._outbound_buffer.extend(other._outbound_buffer) | |||||
def close(self): | |||||
"""Close the event dispatcher.""" | |||||
self.mutex.locked() and self.mutex.release() | |||||
self.producer = None | |||||
def _get_publisher(self): | |||||
return self.producer | |||||
def _set_publisher(self, producer): | |||||
self.producer = producer | |||||
publisher = property(_get_publisher, _set_publisher) # XXX compat | |||||
class EventReceiver(ConsumerMixin): | |||||
"""Capture events. | |||||
:param connection: Connection to the broker. | |||||
:keyword handlers: Event handlers. | |||||
:attr:`handlers` is a dict of event types and their handlers, | |||||
the special handler `"*"` captures all events that doesn't have a | |||||
handler. | |||||
""" | |||||
app = None | |||||
def __init__(self, channel, handlers=None, routing_key='#', | |||||
node_id=None, app=None, queue_prefix='celeryev', | |||||
accept=None): | |||||
self.app = app_or_default(app or self.app) | |||||
self.channel = maybe_channel(channel) | |||||
self.handlers = {} if handlers is None else handlers | |||||
self.routing_key = routing_key | |||||
self.node_id = node_id or uuid() | |||||
self.queue_prefix = queue_prefix | |||||
self.exchange = get_exchange(self.connection or self.app.connection()) | |||||
self.queue = Queue('.'.join([self.queue_prefix, self.node_id]), | |||||
exchange=self.exchange, | |||||
routing_key=self.routing_key, | |||||
auto_delete=True, | |||||
durable=False, | |||||
queue_arguments=self._get_queue_arguments()) | |||||
self.clock = self.app.clock | |||||
self.adjust_clock = self.clock.adjust | |||||
self.forward_clock = self.clock.forward | |||||
if accept is None: | |||||
accept = set([self.app.conf.CELERY_EVENT_SERIALIZER, 'json']) | |||||
self.accept = accept | |||||
def _get_queue_arguments(self): | |||||
conf = self.app.conf | |||||
return dictfilter({ | |||||
'x-message-ttl': maybe_s_to_ms(conf.CELERY_EVENT_QUEUE_TTL), | |||||
'x-expires': maybe_s_to_ms(conf.CELERY_EVENT_QUEUE_EXPIRES), | |||||
}) | |||||
def process(self, type, event): | |||||
"""Process the received event by dispatching it to the appropriate | |||||
handler.""" | |||||
handler = self.handlers.get(type) or self.handlers.get('*') | |||||
handler and handler(event) | |||||
def get_consumers(self, Consumer, channel): | |||||
return [Consumer(queues=[self.queue], | |||||
callbacks=[self._receive], no_ack=True, | |||||
accept=self.accept)] | |||||
def on_consume_ready(self, connection, channel, consumers, | |||||
wakeup=True, **kwargs): | |||||
if wakeup: | |||||
self.wakeup_workers(channel=channel) | |||||
def itercapture(self, limit=None, timeout=None, wakeup=True): | |||||
return self.consume(limit=limit, timeout=timeout, wakeup=wakeup) | |||||
def capture(self, limit=None, timeout=None, wakeup=True): | |||||
"""Open up a consumer capturing events. | |||||
This has to run in the main process, and it will never stop | |||||
unless :attr:`EventDispatcher.should_stop` is set to True, or | |||||
forced via :exc:`KeyboardInterrupt` or :exc:`SystemExit`. | |||||
""" | |||||
return list(self.consume(limit=limit, timeout=timeout, wakeup=wakeup)) | |||||
def wakeup_workers(self, channel=None): | |||||
self.app.control.broadcast('heartbeat', | |||||
connection=self.connection, | |||||
channel=channel) | |||||
def event_from_message(self, body, localize=True, | |||||
now=time.time, tzfields=_TZGETTER, | |||||
adjust_timestamp=adjust_timestamp, | |||||
CLIENT_CLOCK_SKEW=CLIENT_CLOCK_SKEW): | |||||
type = body['type'] | |||||
if type == 'task-sent': | |||||
# clients never sync so cannot use their clock value | |||||
_c = body['clock'] = (self.clock.value or 1) + CLIENT_CLOCK_SKEW | |||||
self.adjust_clock(_c) | |||||
else: | |||||
try: | |||||
clock = body['clock'] | |||||
except KeyError: | |||||
body['clock'] = self.forward_clock() | |||||
else: | |||||
self.adjust_clock(clock) | |||||
if localize: | |||||
try: | |||||
offset, timestamp = tzfields(body) | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
body['timestamp'] = adjust_timestamp(timestamp, offset) | |||||
body['local_received'] = now() | |||||
return type, body | |||||
def _receive(self, body, message): | |||||
self.process(*self.event_from_message(body)) | |||||
@property | |||||
def connection(self): | |||||
return self.channel.connection.client if self.channel else None | |||||
class Events(object): | |||||
def __init__(self, app=None): | |||||
self.app = app | |||||
@cached_property | |||||
def Receiver(self): | |||||
return self.app.subclass_with_self(EventReceiver, | |||||
reverse='events.Receiver') | |||||
@cached_property | |||||
def Dispatcher(self): | |||||
return self.app.subclass_with_self(EventDispatcher, | |||||
reverse='events.Dispatcher') | |||||
@cached_property | |||||
def State(self): | |||||
return self.app.subclass_with_self('celery.events.state:State', | |||||
reverse='events.State') | |||||
@contextmanager | |||||
def default_dispatcher(self, hostname=None, enabled=True, | |||||
buffer_while_offline=False): | |||||
with self.app.amqp.producer_pool.acquire(block=True) as prod: | |||||
with self.Dispatcher(prod.connection, hostname, enabled, | |||||
prod.channel, buffer_while_offline) as d: | |||||
yield d |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.events.cursesmon | |||||
~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Graphical monitor of Celery events using curses. | |||||
""" | |||||
from __future__ import absolute_import, print_function | |||||
import curses | |||||
import sys | |||||
import threading | |||||
from datetime import datetime | |||||
from itertools import count | |||||
from textwrap import wrap | |||||
from time import time | |||||
from math import ceil | |||||
from celery import VERSION_BANNER | |||||
from celery import states | |||||
from celery.app import app_or_default | |||||
from celery.five import items, values | |||||
from celery.utils.text import abbr, abbrtask | |||||
__all__ = ['CursesMonitor', 'evtop'] | |||||
BORDER_SPACING = 4 | |||||
LEFT_BORDER_OFFSET = 3 | |||||
UUID_WIDTH = 36 | |||||
STATE_WIDTH = 8 | |||||
TIMESTAMP_WIDTH = 8 | |||||
MIN_WORKER_WIDTH = 15 | |||||
MIN_TASK_WIDTH = 16 | |||||
# this module is considered experimental | |||||
# we don't care about coverage. | |||||
STATUS_SCREEN = """\ | |||||
events: {s.event_count} tasks:{s.task_count} workers:{w_alive}/{w_all} | |||||
""" | |||||
class CursesMonitor(object): # pragma: no cover | |||||
keymap = {} | |||||
win = None | |||||
screen_width = None | |||||
screen_delay = 10 | |||||
selected_task = None | |||||
selected_position = 0 | |||||
selected_str = 'Selected: ' | |||||
foreground = curses.COLOR_BLACK | |||||
background = curses.COLOR_WHITE | |||||
online_str = 'Workers online: ' | |||||
help_title = 'Keys: ' | |||||
help = ('j:down k:up i:info t:traceback r:result c:revoke ^c: quit') | |||||
greet = 'celery events {0}'.format(VERSION_BANNER) | |||||
info_str = 'Info: ' | |||||
def __init__(self, state, app, keymap=None): | |||||
self.app = app | |||||
self.keymap = keymap or self.keymap | |||||
self.state = state | |||||
default_keymap = {'J': self.move_selection_down, | |||||
'K': self.move_selection_up, | |||||
'C': self.revoke_selection, | |||||
'T': self.selection_traceback, | |||||
'R': self.selection_result, | |||||
'I': self.selection_info, | |||||
'L': self.selection_rate_limit} | |||||
self.keymap = dict(default_keymap, **self.keymap) | |||||
self.lock = threading.RLock() | |||||
def format_row(self, uuid, task, worker, timestamp, state): | |||||
mx = self.display_width | |||||
# include spacing | |||||
detail_width = mx - 1 - STATE_WIDTH - 1 - TIMESTAMP_WIDTH | |||||
uuid_space = detail_width - 1 - MIN_TASK_WIDTH - 1 - MIN_WORKER_WIDTH | |||||
if uuid_space < UUID_WIDTH: | |||||
uuid_width = uuid_space | |||||
else: | |||||
uuid_width = UUID_WIDTH | |||||
detail_width = detail_width - uuid_width - 1 | |||||
task_width = int(ceil(detail_width / 2.0)) | |||||
worker_width = detail_width - task_width - 1 | |||||
uuid = abbr(uuid, uuid_width).ljust(uuid_width) | |||||
worker = abbr(worker, worker_width).ljust(worker_width) | |||||
task = abbrtask(task, task_width).ljust(task_width) | |||||
state = abbr(state, STATE_WIDTH).ljust(STATE_WIDTH) | |||||
timestamp = timestamp.ljust(TIMESTAMP_WIDTH) | |||||
row = '{0} {1} {2} {3} {4} '.format(uuid, worker, task, | |||||
timestamp, state) | |||||
if self.screen_width is None: | |||||
self.screen_width = len(row[:mx]) | |||||
return row[:mx] | |||||
@property | |||||
def screen_width(self): | |||||
_, mx = self.win.getmaxyx() | |||||
return mx | |||||
@property | |||||
def screen_height(self): | |||||
my, _ = self.win.getmaxyx() | |||||
return my | |||||
@property | |||||
def display_width(self): | |||||
_, mx = self.win.getmaxyx() | |||||
return mx - BORDER_SPACING | |||||
@property | |||||
def display_height(self): | |||||
my, _ = self.win.getmaxyx() | |||||
return my - 10 | |||||
@property | |||||
def limit(self): | |||||
return self.display_height | |||||
def find_position(self): | |||||
if not self.tasks: | |||||
return 0 | |||||
for i, e in enumerate(self.tasks): | |||||
if self.selected_task == e[0]: | |||||
return i | |||||
return 0 | |||||
def move_selection_up(self): | |||||
self.move_selection(-1) | |||||
def move_selection_down(self): | |||||
self.move_selection(1) | |||||
def move_selection(self, direction=1): | |||||
if not self.tasks: | |||||
return | |||||
pos = self.find_position() | |||||
try: | |||||
self.selected_task = self.tasks[pos + direction][0] | |||||
except IndexError: | |||||
self.selected_task = self.tasks[0][0] | |||||
keyalias = {curses.KEY_DOWN: 'J', | |||||
curses.KEY_UP: 'K', | |||||
curses.KEY_ENTER: 'I'} | |||||
def handle_keypress(self): | |||||
try: | |||||
key = self.win.getkey().upper() | |||||
except: | |||||
return | |||||
key = self.keyalias.get(key) or key | |||||
handler = self.keymap.get(key) | |||||
if handler is not None: | |||||
handler() | |||||
def alert(self, callback, title=None): | |||||
self.win.erase() | |||||
my, mx = self.win.getmaxyx() | |||||
y = blank_line = count(2) | |||||
if title: | |||||
self.win.addstr(next(y), 3, title, | |||||
curses.A_BOLD | curses.A_UNDERLINE) | |||||
next(blank_line) | |||||
callback(my, mx, next(y)) | |||||
self.win.addstr(my - 1, 0, 'Press any key to continue...', | |||||
curses.A_BOLD) | |||||
self.win.refresh() | |||||
while 1: | |||||
try: | |||||
return self.win.getkey().upper() | |||||
except: | |||||
pass | |||||
def selection_rate_limit(self): | |||||
if not self.selected_task: | |||||
return curses.beep() | |||||
task = self.state.tasks[self.selected_task] | |||||
if not task.name: | |||||
return curses.beep() | |||||
my, mx = self.win.getmaxyx() | |||||
r = 'New rate limit: ' | |||||
self.win.addstr(my - 2, 3, r, curses.A_BOLD | curses.A_UNDERLINE) | |||||
self.win.addstr(my - 2, len(r) + 3, ' ' * (mx - len(r))) | |||||
rlimit = self.readline(my - 2, 3 + len(r)) | |||||
if rlimit: | |||||
reply = self.app.control.rate_limit(task.name, | |||||
rlimit.strip(), reply=True) | |||||
self.alert_remote_control_reply(reply) | |||||
def alert_remote_control_reply(self, reply): | |||||
def callback(my, mx, xs): | |||||
y = count(xs) | |||||
if not reply: | |||||
self.win.addstr( | |||||
next(y), 3, 'No replies received in 1s deadline.', | |||||
curses.A_BOLD + curses.color_pair(2), | |||||
) | |||||
return | |||||
for subreply in reply: | |||||
curline = next(y) | |||||
host, response = next(items(subreply)) | |||||
host = '{0}: '.format(host) | |||||
self.win.addstr(curline, 3, host, curses.A_BOLD) | |||||
attr = curses.A_NORMAL | |||||
text = '' | |||||
if 'error' in response: | |||||
text = response['error'] | |||||
attr |= curses.color_pair(2) | |||||
elif 'ok' in response: | |||||
text = response['ok'] | |||||
attr |= curses.color_pair(3) | |||||
self.win.addstr(curline, 3 + len(host), text, attr) | |||||
return self.alert(callback, 'Remote Control Command Replies') | |||||
def readline(self, x, y): | |||||
buffer = str() | |||||
curses.echo() | |||||
try: | |||||
i = 0 | |||||
while 1: | |||||
ch = self.win.getch(x, y + i) | |||||
if ch != -1: | |||||
if ch in (10, curses.KEY_ENTER): # enter | |||||
break | |||||
if ch in (27, ): | |||||
buffer = str() | |||||
break | |||||
buffer += chr(ch) | |||||
i += 1 | |||||
finally: | |||||
curses.noecho() | |||||
return buffer | |||||
def revoke_selection(self): | |||||
if not self.selected_task: | |||||
return curses.beep() | |||||
reply = self.app.control.revoke(self.selected_task, reply=True) | |||||
self.alert_remote_control_reply(reply) | |||||
def selection_info(self): | |||||
if not self.selected_task: | |||||
return | |||||
def alert_callback(mx, my, xs): | |||||
my, mx = self.win.getmaxyx() | |||||
y = count(xs) | |||||
task = self.state.tasks[self.selected_task] | |||||
info = task.info(extra=['state']) | |||||
infoitems = [ | |||||
('args', info.pop('args', None)), | |||||
('kwargs', info.pop('kwargs', None)) | |||||
] + list(info.items()) | |||||
for key, value in infoitems: | |||||
if key is None: | |||||
continue | |||||
value = str(value) | |||||
curline = next(y) | |||||
keys = key + ': ' | |||||
self.win.addstr(curline, 3, keys, curses.A_BOLD) | |||||
wrapped = wrap(value, mx - 2) | |||||
if len(wrapped) == 1: | |||||
self.win.addstr( | |||||
curline, len(keys) + 3, | |||||
abbr(wrapped[0], | |||||
self.screen_width - (len(keys) + 3))) | |||||
else: | |||||
for subline in wrapped: | |||||
nexty = next(y) | |||||
if nexty >= my - 1: | |||||
subline = ' ' * 4 + '[...]' | |||||
elif nexty >= my: | |||||
break | |||||
self.win.addstr( | |||||
nexty, 3, | |||||
abbr(' ' * 4 + subline, self.screen_width - 4), | |||||
curses.A_NORMAL, | |||||
) | |||||
return self.alert( | |||||
alert_callback, 'Task details for {0.selected_task}'.format(self), | |||||
) | |||||
def selection_traceback(self): | |||||
if not self.selected_task: | |||||
return curses.beep() | |||||
task = self.state.tasks[self.selected_task] | |||||
if task.state not in states.EXCEPTION_STATES: | |||||
return curses.beep() | |||||
def alert_callback(my, mx, xs): | |||||
y = count(xs) | |||||
for line in task.traceback.split('\n'): | |||||
self.win.addstr(next(y), 3, line) | |||||
return self.alert( | |||||
alert_callback, | |||||
'Task Exception Traceback for {0.selected_task}'.format(self), | |||||
) | |||||
def selection_result(self): | |||||
if not self.selected_task: | |||||
return | |||||
def alert_callback(my, mx, xs): | |||||
y = count(xs) | |||||
task = self.state.tasks[self.selected_task] | |||||
result = (getattr(task, 'result', None) or | |||||
getattr(task, 'exception', None)) | |||||
for line in wrap(result or '', mx - 2): | |||||
self.win.addstr(next(y), 3, line) | |||||
return self.alert( | |||||
alert_callback, | |||||
'Task Result for {0.selected_task}'.format(self), | |||||
) | |||||
def display_task_row(self, lineno, task): | |||||
state_color = self.state_colors.get(task.state) | |||||
attr = curses.A_NORMAL | |||||
if task.uuid == self.selected_task: | |||||
attr = curses.A_STANDOUT | |||||
timestamp = datetime.utcfromtimestamp( | |||||
task.timestamp or time(), | |||||
) | |||||
timef = timestamp.strftime('%H:%M:%S') | |||||
hostname = task.worker.hostname if task.worker else '*NONE*' | |||||
line = self.format_row(task.uuid, task.name, | |||||
hostname, | |||||
timef, task.state) | |||||
self.win.addstr(lineno, LEFT_BORDER_OFFSET, line, attr) | |||||
if state_color: | |||||
self.win.addstr(lineno, | |||||
len(line) - STATE_WIDTH + BORDER_SPACING - 1, | |||||
task.state, state_color | attr) | |||||
def draw(self): | |||||
with self.lock: | |||||
win = self.win | |||||
self.handle_keypress() | |||||
x = LEFT_BORDER_OFFSET | |||||
y = blank_line = count(2) | |||||
my, mx = win.getmaxyx() | |||||
win.erase() | |||||
win.bkgd(' ', curses.color_pair(1)) | |||||
win.border() | |||||
win.addstr(1, x, self.greet, curses.A_DIM | curses.color_pair(5)) | |||||
next(blank_line) | |||||
win.addstr(next(y), x, self.format_row('UUID', 'TASK', | |||||
'WORKER', 'TIME', 'STATE'), | |||||
curses.A_BOLD | curses.A_UNDERLINE) | |||||
tasks = self.tasks | |||||
if tasks: | |||||
for row, (uuid, task) in enumerate(tasks): | |||||
if row > self.display_height: | |||||
break | |||||
if task.uuid: | |||||
lineno = next(y) | |||||
self.display_task_row(lineno, task) | |||||
# -- Footer | |||||
next(blank_line) | |||||
win.hline(my - 6, x, curses.ACS_HLINE, self.screen_width - 4) | |||||
# Selected Task Info | |||||
if self.selected_task: | |||||
win.addstr(my - 5, x, self.selected_str, curses.A_BOLD) | |||||
info = 'Missing extended info' | |||||
detail = '' | |||||
try: | |||||
selection = self.state.tasks[self.selected_task] | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
info = selection.info() | |||||
if 'runtime' in info: | |||||
info['runtime'] = '{0:.2f}'.format(info['runtime']) | |||||
if 'result' in info: | |||||
info['result'] = abbr(info['result'], 16) | |||||
info = ' '.join( | |||||
'{0}={1}'.format(key, value) | |||||
for key, value in items(info) | |||||
) | |||||
detail = '... -> key i' | |||||
infowin = abbr(info, | |||||
self.screen_width - len(self.selected_str) - 2, | |||||
detail) | |||||
win.addstr(my - 5, x + len(self.selected_str), infowin) | |||||
# Make ellipsis bold | |||||
if detail in infowin: | |||||
detailpos = len(infowin) - len(detail) | |||||
win.addstr(my - 5, x + len(self.selected_str) + detailpos, | |||||
detail, curses.A_BOLD) | |||||
else: | |||||
win.addstr(my - 5, x, 'No task selected', curses.A_NORMAL) | |||||
# Workers | |||||
if self.workers: | |||||
win.addstr(my - 4, x, self.online_str, curses.A_BOLD) | |||||
win.addstr(my - 4, x + len(self.online_str), | |||||
', '.join(sorted(self.workers)), curses.A_NORMAL) | |||||
else: | |||||
win.addstr(my - 4, x, 'No workers discovered.') | |||||
# Info | |||||
win.addstr(my - 3, x, self.info_str, curses.A_BOLD) | |||||
win.addstr( | |||||
my - 3, x + len(self.info_str), | |||||
STATUS_SCREEN.format( | |||||
s=self.state, | |||||
w_alive=len([w for w in values(self.state.workers) | |||||
if w.alive]), | |||||
w_all=len(self.state.workers), | |||||
), | |||||
curses.A_DIM, | |||||
) | |||||
# Help | |||||
self.safe_add_str(my - 2, x, self.help_title, curses.A_BOLD) | |||||
self.safe_add_str(my - 2, x + len(self.help_title), self.help, | |||||
curses.A_DIM) | |||||
win.refresh() | |||||
def safe_add_str(self, y, x, string, *args, **kwargs): | |||||
if x + len(string) > self.screen_width: | |||||
string = string[:self.screen_width - x] | |||||
self.win.addstr(y, x, string, *args, **kwargs) | |||||
def init_screen(self): | |||||
with self.lock: | |||||
self.win = curses.initscr() | |||||
self.win.nodelay(True) | |||||
self.win.keypad(True) | |||||
curses.start_color() | |||||
curses.init_pair(1, self.foreground, self.background) | |||||
# exception states | |||||
curses.init_pair(2, curses.COLOR_RED, self.background) | |||||
# successful state | |||||
curses.init_pair(3, curses.COLOR_GREEN, self.background) | |||||
# revoked state | |||||
curses.init_pair(4, curses.COLOR_MAGENTA, self.background) | |||||
# greeting | |||||
curses.init_pair(5, curses.COLOR_BLUE, self.background) | |||||
# started state | |||||
curses.init_pair(6, curses.COLOR_YELLOW, self.foreground) | |||||
self.state_colors = {states.SUCCESS: curses.color_pair(3), | |||||
states.REVOKED: curses.color_pair(4), | |||||
states.STARTED: curses.color_pair(6)} | |||||
for state in states.EXCEPTION_STATES: | |||||
self.state_colors[state] = curses.color_pair(2) | |||||
curses.cbreak() | |||||
def resetscreen(self): | |||||
with self.lock: | |||||
curses.nocbreak() | |||||
self.win.keypad(False) | |||||
curses.echo() | |||||
curses.endwin() | |||||
def nap(self): | |||||
curses.napms(self.screen_delay) | |||||
@property | |||||
def tasks(self): | |||||
return list(self.state.tasks_by_time(limit=self.limit)) | |||||
@property | |||||
def workers(self): | |||||
return [hostname for hostname, w in items(self.state.workers) | |||||
if w.alive] | |||||
class DisplayThread(threading.Thread): # pragma: no cover | |||||
def __init__(self, display): | |||||
self.display = display | |||||
self.shutdown = False | |||||
threading.Thread.__init__(self) | |||||
def run(self): | |||||
while not self.shutdown: | |||||
self.display.draw() | |||||
self.display.nap() | |||||
def capture_events(app, state, display): # pragma: no cover | |||||
def on_connection_error(exc, interval): | |||||
print('Connection Error: {0!r}. Retry in {1}s.'.format( | |||||
exc, interval), file=sys.stderr) | |||||
while 1: | |||||
print('-> evtop: starting capture...', file=sys.stderr) | |||||
with app.connection() as conn: | |||||
try: | |||||
conn.ensure_connection(on_connection_error, | |||||
app.conf.BROKER_CONNECTION_MAX_RETRIES) | |||||
recv = app.events.Receiver(conn, handlers={'*': state.event}) | |||||
display.resetscreen() | |||||
display.init_screen() | |||||
recv.capture() | |||||
except conn.connection_errors + conn.channel_errors as exc: | |||||
print('Connection lost: {0!r}'.format(exc), file=sys.stderr) | |||||
def evtop(app=None): # pragma: no cover | |||||
app = app_or_default(app) | |||||
state = app.events.State() | |||||
display = CursesMonitor(state, app) | |||||
display.init_screen() | |||||
refresher = DisplayThread(display) | |||||
refresher.start() | |||||
try: | |||||
capture_events(app, state, display) | |||||
except Exception: | |||||
refresher.shutdown = True | |||||
refresher.join() | |||||
display.resetscreen() | |||||
raise | |||||
except (KeyboardInterrupt, SystemExit): | |||||
refresher.shutdown = True | |||||
refresher.join() | |||||
display.resetscreen() | |||||
if __name__ == '__main__': # pragma: no cover | |||||
evtop() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.events.dumper | |||||
~~~~~~~~~~~~~~~~~~~~ | |||||
This is a simple program that dumps events to the console | |||||
as they happen. Think of it like a `tcpdump` for Celery events. | |||||
""" | |||||
from __future__ import absolute_import, print_function | |||||
import sys | |||||
from datetime import datetime | |||||
from celery.app import app_or_default | |||||
from celery.utils.functional import LRUCache | |||||
from celery.utils.timeutils import humanize_seconds | |||||
__all__ = ['Dumper', 'evdump'] | |||||
TASK_NAMES = LRUCache(limit=0xFFF) | |||||
HUMAN_TYPES = {'worker-offline': 'shutdown', | |||||
'worker-online': 'started', | |||||
'worker-heartbeat': 'heartbeat'} | |||||
CONNECTION_ERROR = """\ | |||||
-> Cannot connect to %s: %s. | |||||
Trying again %s | |||||
""" | |||||
def humanize_type(type): | |||||
try: | |||||
return HUMAN_TYPES[type.lower()] | |||||
except KeyError: | |||||
return type.lower().replace('-', ' ') | |||||
class Dumper(object): | |||||
def __init__(self, out=sys.stdout): | |||||
self.out = out | |||||
def say(self, msg): | |||||
print(msg, file=self.out) | |||||
# need to flush so that output can be piped. | |||||
try: | |||||
self.out.flush() | |||||
except AttributeError: | |||||
pass | |||||
def on_event(self, ev): | |||||
timestamp = datetime.utcfromtimestamp(ev.pop('timestamp')) | |||||
type = ev.pop('type').lower() | |||||
hostname = ev.pop('hostname') | |||||
if type.startswith('task-'): | |||||
uuid = ev.pop('uuid') | |||||
if type in ('task-received', 'task-sent'): | |||||
task = TASK_NAMES[uuid] = '{0}({1}) args={2} kwargs={3}' \ | |||||
.format(ev.pop('name'), uuid, | |||||
ev.pop('args'), | |||||
ev.pop('kwargs')) | |||||
else: | |||||
task = TASK_NAMES.get(uuid, '') | |||||
return self.format_task_event(hostname, timestamp, | |||||
type, task, ev) | |||||
fields = ', '.join( | |||||
'{0}={1}'.format(key, ev[key]) for key in sorted(ev) | |||||
) | |||||
sep = fields and ':' or '' | |||||
self.say('{0} [{1}] {2}{3} {4}'.format( | |||||
hostname, timestamp, humanize_type(type), sep, fields), | |||||
) | |||||
def format_task_event(self, hostname, timestamp, type, task, event): | |||||
fields = ', '.join( | |||||
'{0}={1}'.format(key, event[key]) for key in sorted(event) | |||||
) | |||||
sep = fields and ':' or '' | |||||
self.say('{0} [{1}] {2}{3} {4} {5}'.format( | |||||
hostname, timestamp, humanize_type(type), sep, task, fields), | |||||
) | |||||
def evdump(app=None, out=sys.stdout): | |||||
app = app_or_default(app) | |||||
dumper = Dumper(out=out) | |||||
dumper.say('-> evdump: starting capture...') | |||||
conn = app.connection().clone() | |||||
def _error_handler(exc, interval): | |||||
dumper.say(CONNECTION_ERROR % ( | |||||
conn.as_uri(), exc, humanize_seconds(interval, 'in', ' ') | |||||
)) | |||||
while 1: | |||||
try: | |||||
conn.ensure_connection(_error_handler) | |||||
recv = app.events.Receiver(conn, handlers={'*': dumper.on_event}) | |||||
recv.capture() | |||||
except (KeyboardInterrupt, SystemExit): | |||||
return conn and conn.close() | |||||
except conn.connection_errors + conn.channel_errors: | |||||
dumper.say('-> Connection lost, attempting reconnect') | |||||
if __name__ == '__main__': # pragma: no cover | |||||
evdump() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.events.snapshot | |||||
~~~~~~~~~~~~~~~~~~~~~~ | |||||
Consuming the events as a stream is not always suitable | |||||
so this module implements a system to take snapshots of the | |||||
state of a cluster at regular intervals. There is a full | |||||
implementation of this writing the snapshots to a database | |||||
in :mod:`djcelery.snapshots` in the `django-celery` distribution. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from kombu.utils.limits import TokenBucket | |||||
from celery import platforms | |||||
from celery.app import app_or_default | |||||
from celery.utils.timer2 import Timer | |||||
from celery.utils.dispatch import Signal | |||||
from celery.utils.imports import instantiate | |||||
from celery.utils.log import get_logger | |||||
from celery.utils.timeutils import rate | |||||
__all__ = ['Polaroid', 'evcam'] | |||||
logger = get_logger('celery.evcam') | |||||
class Polaroid(object): | |||||
timer = None | |||||
shutter_signal = Signal(providing_args=('state', )) | |||||
cleanup_signal = Signal() | |||||
clear_after = False | |||||
_tref = None | |||||
_ctref = None | |||||
def __init__(self, state, freq=1.0, maxrate=None, | |||||
cleanup_freq=3600.0, timer=None, app=None): | |||||
self.app = app_or_default(app) | |||||
self.state = state | |||||
self.freq = freq | |||||
self.cleanup_freq = cleanup_freq | |||||
self.timer = timer or self.timer or Timer() | |||||
self.logger = logger | |||||
self.maxrate = maxrate and TokenBucket(rate(maxrate)) | |||||
def install(self): | |||||
self._tref = self.timer.call_repeatedly(self.freq, self.capture) | |||||
self._ctref = self.timer.call_repeatedly( | |||||
self.cleanup_freq, self.cleanup, | |||||
) | |||||
def on_shutter(self, state): | |||||
pass | |||||
def on_cleanup(self): | |||||
pass | |||||
def cleanup(self): | |||||
logger.debug('Cleanup: Running...') | |||||
self.cleanup_signal.send(None) | |||||
self.on_cleanup() | |||||
def shutter(self): | |||||
if self.maxrate is None or self.maxrate.can_consume(): | |||||
logger.debug('Shutter: %s', self.state) | |||||
self.shutter_signal.send(self.state) | |||||
self.on_shutter(self.state) | |||||
def capture(self): | |||||
self.state.freeze_while(self.shutter, clear_after=self.clear_after) | |||||
def cancel(self): | |||||
if self._tref: | |||||
self._tref() # flush all received events. | |||||
self._tref.cancel() | |||||
if self._ctref: | |||||
self._ctref.cancel() | |||||
def __enter__(self): | |||||
self.install() | |||||
return self | |||||
def __exit__(self, *exc_info): | |||||
self.cancel() | |||||
def evcam(camera, freq=1.0, maxrate=None, loglevel=0, | |||||
logfile=None, pidfile=None, timer=None, app=None): | |||||
app = app_or_default(app) | |||||
if pidfile: | |||||
platforms.create_pidlock(pidfile) | |||||
app.log.setup_logging_subsystem(loglevel, logfile) | |||||
print('-> evcam: Taking snapshots with {0} (every {1} secs.)'.format( | |||||
camera, freq)) | |||||
state = app.events.State() | |||||
cam = instantiate(camera, state, app=app, freq=freq, | |||||
maxrate=maxrate, timer=timer) | |||||
cam.install() | |||||
conn = app.connection() | |||||
recv = app.events.Receiver(conn, handlers={'*': state.event}) | |||||
try: | |||||
try: | |||||
recv.capture(limit=None) | |||||
except KeyboardInterrupt: | |||||
raise SystemExit | |||||
finally: | |||||
cam.cancel() | |||||
conn.close() |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.events.state | |||||
~~~~~~~~~~~~~~~~~~~ | |||||
This module implements a datastructure used to keep | |||||
track of the state of a cluster of workers and the tasks | |||||
it is working on (by consuming events). | |||||
For every event consumed the state is updated, | |||||
so the state represents the state of the cluster | |||||
at the time of the last event. | |||||
Snapshots (:mod:`celery.events.snapshot`) can be used to | |||||
take "pictures" of this state at regular intervals | |||||
to e.g. store that in a database. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import bisect | |||||
import sys | |||||
import threading | |||||
from datetime import datetime | |||||
from decimal import Decimal | |||||
from itertools import islice | |||||
from operator import itemgetter | |||||
from time import time | |||||
from weakref import ref | |||||
from kombu.clocks import timetuple | |||||
from kombu.utils import cached_property, kwdict | |||||
from celery import states | |||||
from celery.five import class_property, items, values | |||||
from celery.utils import deprecated | |||||
from celery.utils.functional import LRUCache, memoize | |||||
from celery.utils.log import get_logger | |||||
PYPY = hasattr(sys, 'pypy_version_info') | |||||
# The window (in percentage) is added to the workers heartbeat | |||||
# frequency. If the time between updates exceeds this window, | |||||
# then the worker is considered to be offline. | |||||
HEARTBEAT_EXPIRE_WINDOW = 200 | |||||
# Max drift between event timestamp and time of event received | |||||
# before we alert that clocks may be unsynchronized. | |||||
HEARTBEAT_DRIFT_MAX = 16 | |||||
DRIFT_WARNING = """\ | |||||
Substantial drift from %s may mean clocks are out of sync. Current drift is | |||||
%s seconds. [orig: %s recv: %s] | |||||
""" | |||||
CAN_KWDICT = sys.version_info >= (2, 6, 5) | |||||
logger = get_logger(__name__) | |||||
warn = logger.warning | |||||
R_STATE = '<State: events={0.event_count} tasks={0.task_count}>' | |||||
R_WORKER = '<Worker: {0.hostname} ({0.status_string} clock:{0.clock})' | |||||
R_TASK = '<Task: {0.name}({0.uuid}) {0.state} clock:{0.clock}>' | |||||
__all__ = ['Worker', 'Task', 'State', 'heartbeat_expires'] | |||||
@memoize(maxsize=1000, keyfun=lambda a, _: a[0]) | |||||
def _warn_drift(hostname, drift, local_received, timestamp): | |||||
# we use memoize here so the warning is only logged once per hostname | |||||
warn(DRIFT_WARNING, hostname, drift, | |||||
datetime.fromtimestamp(local_received), | |||||
datetime.fromtimestamp(timestamp)) | |||||
def heartbeat_expires(timestamp, freq=60, | |||||
expire_window=HEARTBEAT_EXPIRE_WINDOW, | |||||
Decimal=Decimal, float=float, isinstance=isinstance): | |||||
# some json implementations returns decimal.Decimal objects, | |||||
# which are not compatible with float. | |||||
freq = float(freq) if isinstance(freq, Decimal) else freq | |||||
if isinstance(timestamp, Decimal): | |||||
timestamp = float(timestamp) | |||||
return timestamp + (freq * (expire_window / 1e2)) | |||||
def _depickle_task(cls, fields): | |||||
return cls(**(fields if CAN_KWDICT else kwdict(fields))) | |||||
def with_unique_field(attr): | |||||
def _decorate_cls(cls): | |||||
def __eq__(this, other): | |||||
if isinstance(other, this.__class__): | |||||
return getattr(this, attr) == getattr(other, attr) | |||||
return NotImplemented | |||||
cls.__eq__ = __eq__ | |||||
def __ne__(this, other): | |||||
return not this.__eq__(other) | |||||
cls.__ne__ = __ne__ | |||||
def __hash__(this): | |||||
return hash(getattr(this, attr)) | |||||
cls.__hash__ = __hash__ | |||||
return cls | |||||
return _decorate_cls | |||||
@with_unique_field('hostname') | |||||
class Worker(object): | |||||
"""Worker State.""" | |||||
heartbeat_max = 4 | |||||
expire_window = HEARTBEAT_EXPIRE_WINDOW | |||||
_fields = ('hostname', 'pid', 'freq', 'heartbeats', 'clock', | |||||
'active', 'processed', 'loadavg', 'sw_ident', | |||||
'sw_ver', 'sw_sys') | |||||
if not PYPY: | |||||
__slots__ = _fields + ('event', '__dict__', '__weakref__') | |||||
def __init__(self, hostname=None, pid=None, freq=60, | |||||
heartbeats=None, clock=0, active=None, processed=None, | |||||
loadavg=None, sw_ident=None, sw_ver=None, sw_sys=None): | |||||
self.hostname = hostname | |||||
self.pid = pid | |||||
self.freq = freq | |||||
self.heartbeats = [] if heartbeats is None else heartbeats | |||||
self.clock = clock or 0 | |||||
self.active = active | |||||
self.processed = processed | |||||
self.loadavg = loadavg | |||||
self.sw_ident = sw_ident | |||||
self.sw_ver = sw_ver | |||||
self.sw_sys = sw_sys | |||||
self.event = self._create_event_handler() | |||||
def __reduce__(self): | |||||
return self.__class__, (self.hostname, self.pid, self.freq, | |||||
self.heartbeats, self.clock, self.active, | |||||
self.processed, self.loadavg, self.sw_ident, | |||||
self.sw_ver, self.sw_sys) | |||||
def _create_event_handler(self): | |||||
_set = object.__setattr__ | |||||
hbmax = self.heartbeat_max | |||||
heartbeats = self.heartbeats | |||||
hb_pop = self.heartbeats.pop | |||||
hb_append = self.heartbeats.append | |||||
def event(type_, timestamp=None, | |||||
local_received=None, fields=None, | |||||
max_drift=HEARTBEAT_DRIFT_MAX, items=items, abs=abs, int=int, | |||||
insort=bisect.insort, len=len): | |||||
fields = fields or {} | |||||
for k, v in items(fields): | |||||
_set(self, k, v) | |||||
if type_ == 'offline': | |||||
heartbeats[:] = [] | |||||
else: | |||||
if not local_received or not timestamp: | |||||
return | |||||
drift = abs(int(local_received) - int(timestamp)) | |||||
if drift > HEARTBEAT_DRIFT_MAX: | |||||
_warn_drift(self.hostname, drift, | |||||
local_received, timestamp) | |||||
if local_received: | |||||
hearts = len(heartbeats) | |||||
if hearts > hbmax - 1: | |||||
hb_pop(0) | |||||
if hearts and local_received > heartbeats[-1]: | |||||
hb_append(local_received) | |||||
else: | |||||
insort(heartbeats, local_received) | |||||
return event | |||||
def update(self, f, **kw): | |||||
for k, v in items(dict(f, **kw) if kw else f): | |||||
setattr(self, k, v) | |||||
def __repr__(self): | |||||
return R_WORKER.format(self) | |||||
@property | |||||
def status_string(self): | |||||
return 'ONLINE' if self.alive else 'OFFLINE' | |||||
@property | |||||
def heartbeat_expires(self): | |||||
return heartbeat_expires(self.heartbeats[-1], | |||||
self.freq, self.expire_window) | |||||
@property | |||||
def alive(self, nowfun=time): | |||||
return bool(self.heartbeats and nowfun() < self.heartbeat_expires) | |||||
@property | |||||
def id(self): | |||||
return '{0.hostname}.{0.pid}'.format(self) | |||||
@deprecated(3.2, 3.3) | |||||
def update_heartbeat(self, received, timestamp): | |||||
self.event(None, timestamp, received) | |||||
@deprecated(3.2, 3.3) | |||||
def on_online(self, timestamp=None, local_received=None, **fields): | |||||
self.event('online', timestamp, local_received, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_offline(self, timestamp=None, local_received=None, **fields): | |||||
self.event('offline', timestamp, local_received, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_heartbeat(self, timestamp=None, local_received=None, **fields): | |||||
self.event('heartbeat', timestamp, local_received, fields) | |||||
@class_property | |||||
def _defaults(cls): | |||||
"""Deprecated, to be removed in 3.3""" | |||||
source = cls() | |||||
return dict((k, getattr(source, k)) for k in cls._fields) | |||||
@with_unique_field('uuid') | |||||
class Task(object): | |||||
"""Task State.""" | |||||
name = received = sent = started = succeeded = failed = retried = \ | |||||
revoked = args = kwargs = eta = expires = retries = worker = result = \ | |||||
exception = timestamp = runtime = traceback = exchange = \ | |||||
routing_key = client = None | |||||
state = states.PENDING | |||||
clock = 0 | |||||
_fields = ('uuid', 'name', 'state', 'received', 'sent', 'started', | |||||
'succeeded', 'failed', 'retried', 'revoked', 'args', 'kwargs', | |||||
'eta', 'expires', 'retries', 'worker', 'result', 'exception', | |||||
'timestamp', 'runtime', 'traceback', 'exchange', 'routing_key', | |||||
'clock', 'client') | |||||
if not PYPY: | |||||
__slots__ = ('__dict__', '__weakref__') | |||||
#: How to merge out of order events. | |||||
#: Disorder is detected by logical ordering (e.g. :event:`task-received` | |||||
#: must have happened before a :event:`task-failed` event). | |||||
#: | |||||
#: A merge rule consists of a state and a list of fields to keep from | |||||
#: that state. ``(RECEIVED, ('name', 'args')``, means the name and args | |||||
#: fields are always taken from the RECEIVED state, and any values for | |||||
#: these fields received before or after is simply ignored. | |||||
merge_rules = {states.RECEIVED: ('name', 'args', 'kwargs', | |||||
'retries', 'eta', 'expires')} | |||||
#: meth:`info` displays these fields by default. | |||||
_info_fields = ('args', 'kwargs', 'retries', 'result', 'eta', 'runtime', | |||||
'expires', 'exception', 'exchange', 'routing_key') | |||||
def __init__(self, uuid=None, **kwargs): | |||||
self.uuid = uuid | |||||
if kwargs: | |||||
for k, v in items(kwargs): | |||||
setattr(self, k, v) | |||||
def event(self, type_, timestamp=None, local_received=None, fields=None, | |||||
precedence=states.precedence, items=items, dict=dict, | |||||
PENDING=states.PENDING, RECEIVED=states.RECEIVED, | |||||
STARTED=states.STARTED, FAILURE=states.FAILURE, | |||||
RETRY=states.RETRY, SUCCESS=states.SUCCESS, | |||||
REVOKED=states.REVOKED): | |||||
fields = fields or {} | |||||
if type_ == 'sent': | |||||
state, self.sent = PENDING, timestamp | |||||
elif type_ == 'received': | |||||
state, self.received = RECEIVED, timestamp | |||||
elif type_ == 'started': | |||||
state, self.started = STARTED, timestamp | |||||
elif type_ == 'failed': | |||||
state, self.failed = FAILURE, timestamp | |||||
elif type_ == 'retried': | |||||
state, self.retried = RETRY, timestamp | |||||
elif type_ == 'succeeded': | |||||
state, self.succeeded = SUCCESS, timestamp | |||||
elif type_ == 'revoked': | |||||
state, self.revoked = REVOKED, timestamp | |||||
else: | |||||
state = type_.upper() | |||||
# note that precedence here is reversed | |||||
# see implementation in celery.states.state.__lt__ | |||||
if state != RETRY and self.state != RETRY and \ | |||||
precedence(state) > precedence(self.state): | |||||
# this state logically happens-before the current state, so merge. | |||||
keep = self.merge_rules.get(state) | |||||
if keep is not None: | |||||
fields = dict( | |||||
(k, v) for k, v in items(fields) if k in keep | |||||
) | |||||
for key, value in items(fields): | |||||
setattr(self, key, value) | |||||
else: | |||||
self.state = state | |||||
self.timestamp = timestamp | |||||
for key, value in items(fields): | |||||
setattr(self, key, value) | |||||
def info(self, fields=None, extra=[]): | |||||
"""Information about this task suitable for on-screen display.""" | |||||
fields = self._info_fields if fields is None else fields | |||||
def _keys(): | |||||
for key in list(fields) + list(extra): | |||||
value = getattr(self, key, None) | |||||
if value is not None: | |||||
yield key, value | |||||
return dict(_keys()) | |||||
def __repr__(self): | |||||
return R_TASK.format(self) | |||||
def as_dict(self): | |||||
get = object.__getattribute__ | |||||
return dict( | |||||
(k, get(self, k)) for k in self._fields | |||||
) | |||||
def __reduce__(self): | |||||
return _depickle_task, (self.__class__, self.as_dict()) | |||||
@property | |||||
def origin(self): | |||||
return self.client if self.worker is None else self.worker.id | |||||
@property | |||||
def ready(self): | |||||
return self.state in states.READY_STATES | |||||
@deprecated(3.2, 3.3) | |||||
def on_sent(self, timestamp=None, **fields): | |||||
self.event('sent', timestamp, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_received(self, timestamp=None, **fields): | |||||
self.event('received', timestamp, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_started(self, timestamp=None, **fields): | |||||
self.event('started', timestamp, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_failed(self, timestamp=None, **fields): | |||||
self.event('failed', timestamp, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_retried(self, timestamp=None, **fields): | |||||
self.event('retried', timestamp, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_succeeded(self, timestamp=None, **fields): | |||||
self.event('succeeded', timestamp, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_revoked(self, timestamp=None, **fields): | |||||
self.event('revoked', timestamp, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def on_unknown_event(self, shortype, timestamp=None, **fields): | |||||
self.event(shortype, timestamp, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def update(self, state, timestamp, fields, | |||||
_state=states.state, RETRY=states.RETRY): | |||||
return self.event(state, timestamp, None, fields) | |||||
@deprecated(3.2, 3.3) | |||||
def merge(self, state, timestamp, fields): | |||||
keep = self.merge_rules.get(state) | |||||
if keep is not None: | |||||
fields = dict((k, v) for k, v in items(fields) if k in keep) | |||||
for key, value in items(fields): | |||||
setattr(self, key, value) | |||||
@class_property | |||||
def _defaults(cls): | |||||
"""Deprecated, to be removed in 3.3.""" | |||||
source = cls() | |||||
return dict((k, getattr(source, k)) for k in source._fields) | |||||
class State(object): | |||||
"""Records clusters state.""" | |||||
Worker = Worker | |||||
Task = Task | |||||
event_count = 0 | |||||
task_count = 0 | |||||
heap_multiplier = 4 | |||||
def __init__(self, callback=None, | |||||
workers=None, tasks=None, taskheap=None, | |||||
max_workers_in_memory=5000, max_tasks_in_memory=10000, | |||||
on_node_join=None, on_node_leave=None): | |||||
self.event_callback = callback | |||||
self.workers = (LRUCache(max_workers_in_memory) | |||||
if workers is None else workers) | |||||
self.tasks = (LRUCache(max_tasks_in_memory) | |||||
if tasks is None else tasks) | |||||
self._taskheap = [] if taskheap is None else taskheap | |||||
self.max_workers_in_memory = max_workers_in_memory | |||||
self.max_tasks_in_memory = max_tasks_in_memory | |||||
self.on_node_join = on_node_join | |||||
self.on_node_leave = on_node_leave | |||||
self._mutex = threading.Lock() | |||||
self.handlers = {} | |||||
self._seen_types = set() | |||||
self.rebuild_taskheap() | |||||
@cached_property | |||||
def _event(self): | |||||
return self._create_dispatcher() | |||||
def freeze_while(self, fun, *args, **kwargs): | |||||
clear_after = kwargs.pop('clear_after', False) | |||||
with self._mutex: | |||||
try: | |||||
return fun(*args, **kwargs) | |||||
finally: | |||||
if clear_after: | |||||
self._clear() | |||||
def clear_tasks(self, ready=True): | |||||
with self._mutex: | |||||
return self._clear_tasks(ready) | |||||
def _clear_tasks(self, ready=True): | |||||
if ready: | |||||
in_progress = dict( | |||||
(uuid, task) for uuid, task in self.itertasks() | |||||
if task.state not in states.READY_STATES) | |||||
self.tasks.clear() | |||||
self.tasks.update(in_progress) | |||||
else: | |||||
self.tasks.clear() | |||||
self._taskheap[:] = [] | |||||
def _clear(self, ready=True): | |||||
self.workers.clear() | |||||
self._clear_tasks(ready) | |||||
self.event_count = 0 | |||||
self.task_count = 0 | |||||
def clear(self, ready=True): | |||||
with self._mutex: | |||||
return self._clear(ready) | |||||
def get_or_create_worker(self, hostname, **kwargs): | |||||
"""Get or create worker by hostname. | |||||
Return tuple of ``(worker, was_created)``. | |||||
""" | |||||
try: | |||||
worker = self.workers[hostname] | |||||
if kwargs: | |||||
worker.update(kwargs) | |||||
return worker, False | |||||
except KeyError: | |||||
worker = self.workers[hostname] = self.Worker( | |||||
hostname, **kwargs) | |||||
return worker, True | |||||
def get_or_create_task(self, uuid): | |||||
"""Get or create task by uuid.""" | |||||
try: | |||||
return self.tasks[uuid], False | |||||
except KeyError: | |||||
task = self.tasks[uuid] = self.Task(uuid) | |||||
return task, True | |||||
def event(self, event): | |||||
with self._mutex: | |||||
return self._event(event) | |||||
def task_event(self, type_, fields): | |||||
"""Deprecated, use :meth:`event`.""" | |||||
return self._event(dict(fields, type='-'.join(['task', type_])))[0] | |||||
def worker_event(self, type_, fields): | |||||
"""Deprecated, use :meth:`event`.""" | |||||
return self._event(dict(fields, type='-'.join(['worker', type_])))[0] | |||||
def _create_dispatcher(self): | |||||
get_handler = self.handlers.__getitem__ | |||||
event_callback = self.event_callback | |||||
wfields = itemgetter('hostname', 'timestamp', 'local_received') | |||||
tfields = itemgetter('uuid', 'hostname', 'timestamp', | |||||
'local_received', 'clock') | |||||
taskheap = self._taskheap | |||||
th_append = taskheap.append | |||||
th_pop = taskheap.pop | |||||
# Removing events from task heap is an O(n) operation, | |||||
# so easier to just account for the common number of events | |||||
# for each task (PENDING->RECEIVED->STARTED->final) | |||||
#: an O(n) operation | |||||
max_events_in_heap = self.max_tasks_in_memory * self.heap_multiplier | |||||
add_type = self._seen_types.add | |||||
on_node_join, on_node_leave = self.on_node_join, self.on_node_leave | |||||
tasks, Task = self.tasks, self.Task | |||||
workers, Worker = self.workers, self.Worker | |||||
# avoid updating LRU entry at getitem | |||||
get_worker, get_task = workers.data.__getitem__, tasks.data.__getitem__ | |||||
def _event(event, | |||||
timetuple=timetuple, KeyError=KeyError, | |||||
insort=bisect.insort, created=True): | |||||
self.event_count += 1 | |||||
if event_callback: | |||||
event_callback(self, event) | |||||
group, _, subject = event['type'].partition('-') | |||||
try: | |||||
handler = get_handler(group) | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
return handler(subject, event), subject | |||||
if group == 'worker': | |||||
try: | |||||
hostname, timestamp, local_received = wfields(event) | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
is_offline = subject == 'offline' | |||||
try: | |||||
worker, created = get_worker(hostname), False | |||||
except KeyError: | |||||
if is_offline: | |||||
worker, created = Worker(hostname), False | |||||
else: | |||||
worker = workers[hostname] = Worker(hostname) | |||||
worker.event(subject, timestamp, local_received, event) | |||||
if on_node_join and (created or subject == 'online'): | |||||
on_node_join(worker) | |||||
if on_node_leave and is_offline: | |||||
on_node_leave(worker) | |||||
workers.pop(hostname, None) | |||||
return (worker, created), subject | |||||
elif group == 'task': | |||||
(uuid, hostname, timestamp, | |||||
local_received, clock) = tfields(event) | |||||
# task-sent event is sent by client, not worker | |||||
is_client_event = subject == 'sent' | |||||
try: | |||||
task, created = get_task(uuid), False | |||||
except KeyError: | |||||
task = tasks[uuid] = Task(uuid) | |||||
if is_client_event: | |||||
task.client = hostname | |||||
else: | |||||
try: | |||||
worker, created = get_worker(hostname), False | |||||
except KeyError: | |||||
worker = workers[hostname] = Worker(hostname) | |||||
task.worker = worker | |||||
if worker is not None and local_received: | |||||
worker.event(None, local_received, timestamp) | |||||
origin = hostname if is_client_event else worker.id | |||||
# remove oldest event if exceeding the limit. | |||||
heaps = len(taskheap) | |||||
if heaps + 1 > max_events_in_heap: | |||||
th_pop(0) | |||||
# most events will be dated later than the previous. | |||||
timetup = timetuple(clock, timestamp, origin, ref(task)) | |||||
if heaps and timetup > taskheap[-1]: | |||||
th_append(timetup) | |||||
else: | |||||
insort(taskheap, timetup) | |||||
if subject == 'received': | |||||
self.task_count += 1 | |||||
task.event(subject, timestamp, local_received, event) | |||||
task_name = task.name | |||||
if task_name is not None: | |||||
add_type(task_name) | |||||
return (task, created), subject | |||||
return _event | |||||
def rebuild_taskheap(self, timetuple=timetuple): | |||||
heap = self._taskheap[:] = [ | |||||
timetuple(t.clock, t.timestamp, t.origin, ref(t)) | |||||
for t in values(self.tasks) | |||||
] | |||||
heap.sort() | |||||
def itertasks(self, limit=None): | |||||
for index, row in enumerate(items(self.tasks)): | |||||
yield row | |||||
if limit and index + 1 >= limit: | |||||
break | |||||
def tasks_by_time(self, limit=None): | |||||
"""Generator giving tasks ordered by time, | |||||
in ``(uuid, Task)`` tuples.""" | |||||
seen = set() | |||||
for evtup in islice(reversed(self._taskheap), 0, limit): | |||||
task = evtup[3]() | |||||
if task is not None: | |||||
uuid = task.uuid | |||||
if uuid not in seen: | |||||
yield uuid, task | |||||
seen.add(uuid) | |||||
tasks_by_timestamp = tasks_by_time | |||||
def tasks_by_type(self, name, limit=None): | |||||
"""Get all tasks by type. | |||||
Return a list of ``(uuid, Task)`` tuples. | |||||
""" | |||||
return islice( | |||||
((uuid, task) for uuid, task in self.tasks_by_time() | |||||
if task.name == name), | |||||
0, limit, | |||||
) | |||||
def tasks_by_worker(self, hostname, limit=None): | |||||
"""Get all tasks by worker. | |||||
""" | |||||
return islice( | |||||
((uuid, task) for uuid, task in self.tasks_by_time() | |||||
if task.worker.hostname == hostname), | |||||
0, limit, | |||||
) | |||||
def task_types(self): | |||||
"""Return a list of all seen task types.""" | |||||
return sorted(self._seen_types) | |||||
def alive_workers(self): | |||||
"""Return a list of (seemingly) alive workers.""" | |||||
return [w for w in values(self.workers) if w.alive] | |||||
def __repr__(self): | |||||
return R_STATE.format(self) | |||||
def __reduce__(self): | |||||
return self.__class__, ( | |||||
self.event_callback, self.workers, self.tasks, None, | |||||
self.max_workers_in_memory, self.max_tasks_in_memory, | |||||
self.on_node_join, self.on_node_leave, | |||||
) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.exceptions | |||||
~~~~~~~~~~~~~~~~~ | |||||
This module contains all exceptions used by the Celery API. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import numbers | |||||
from .five import string_t | |||||
from billiard.exceptions import ( # noqa | |||||
SoftTimeLimitExceeded, TimeLimitExceeded, WorkerLostError, Terminated, | |||||
) | |||||
__all__ = ['SecurityError', 'Ignore', 'QueueNotFound', | |||||
'WorkerShutdown', 'WorkerTerminate', | |||||
'ImproperlyConfigured', 'NotRegistered', 'AlreadyRegistered', | |||||
'TimeoutError', 'MaxRetriesExceededError', 'Retry', | |||||
'TaskRevokedError', 'NotConfigured', 'AlwaysEagerIgnored', | |||||
'InvalidTaskError', 'ChordError', 'CPendingDeprecationWarning', | |||||
'CDeprecationWarning', 'FixupWarning', 'DuplicateNodenameWarning', | |||||
'SoftTimeLimitExceeded', 'TimeLimitExceeded', 'WorkerLostError', | |||||
'Terminated'] | |||||
UNREGISTERED_FMT = """\ | |||||
Task of kind {0} is not registered, please make sure it's imported.\ | |||||
""" | |||||
class SecurityError(Exception): | |||||
"""Security related exceptions. | |||||
Handle with care. | |||||
""" | |||||
class Ignore(Exception): | |||||
"""A task can raise this to ignore doing state updates.""" | |||||
class Reject(Exception): | |||||
"""A task can raise this if it wants to reject/requeue the message.""" | |||||
def __init__(self, reason=None, requeue=False): | |||||
self.reason = reason | |||||
self.requeue = requeue | |||||
super(Reject, self).__init__(reason, requeue) | |||||
def __repr__(self): | |||||
return 'reject requeue=%s: %s' % (self.requeue, self.reason) | |||||
class WorkerTerminate(SystemExit): | |||||
"""Signals that the worker should terminate immediately.""" | |||||
SystemTerminate = WorkerTerminate # XXX compat | |||||
class WorkerShutdown(SystemExit): | |||||
"""Signals that the worker should perform a warm shutdown.""" | |||||
class QueueNotFound(KeyError): | |||||
"""Task routed to a queue not in CELERY_QUEUES.""" | |||||
class ImproperlyConfigured(ImportError): | |||||
"""Celery is somehow improperly configured.""" | |||||
class NotRegistered(KeyError): | |||||
"""The task is not registered.""" | |||||
def __repr__(self): | |||||
return UNREGISTERED_FMT.format(self) | |||||
class AlreadyRegistered(Exception): | |||||
"""The task is already registered.""" | |||||
class TimeoutError(Exception): | |||||
"""The operation timed out.""" | |||||
class MaxRetriesExceededError(Exception): | |||||
"""The tasks max restart limit has been exceeded.""" | |||||
class Retry(Exception): | |||||
"""The task is to be retried later.""" | |||||
#: Optional message describing context of retry. | |||||
message = None | |||||
#: Exception (if any) that caused the retry to happen. | |||||
exc = None | |||||
#: Time of retry (ETA), either :class:`numbers.Real` or | |||||
#: :class:`~datetime.datetime`. | |||||
when = None | |||||
def __init__(self, message=None, exc=None, when=None, **kwargs): | |||||
from kombu.utils.encoding import safe_repr | |||||
self.message = message | |||||
if isinstance(exc, string_t): | |||||
self.exc, self.excs = None, exc | |||||
else: | |||||
self.exc, self.excs = exc, safe_repr(exc) if exc else None | |||||
self.when = when | |||||
Exception.__init__(self, exc, when, **kwargs) | |||||
def humanize(self): | |||||
if isinstance(self.when, numbers.Real): | |||||
return 'in {0.when}s'.format(self) | |||||
return 'at {0.when}'.format(self) | |||||
def __str__(self): | |||||
if self.message: | |||||
return self.message | |||||
if self.excs: | |||||
return 'Retry {0}: {1}'.format(self.humanize(), self.excs) | |||||
return 'Retry {0}'.format(self.humanize()) | |||||
def __reduce__(self): | |||||
return self.__class__, (self.message, self.excs, self.when) | |||||
RetryTaskError = Retry # XXX compat | |||||
class TaskRevokedError(Exception): | |||||
"""The task has been revoked, so no result available.""" | |||||
class NotConfigured(UserWarning): | |||||
"""Celery has not been configured, as no config module has been found.""" | |||||
class AlwaysEagerIgnored(UserWarning): | |||||
"""send_task ignores CELERY_ALWAYS_EAGER option""" | |||||
class InvalidTaskError(Exception): | |||||
"""The task has invalid data or is not properly constructed.""" | |||||
class IncompleteStream(Exception): | |||||
"""Found the end of a stream of data, but the data is not yet complete.""" | |||||
class ChordError(Exception): | |||||
"""A task part of the chord raised an exception.""" | |||||
class CPendingDeprecationWarning(PendingDeprecationWarning): | |||||
pass | |||||
class CDeprecationWarning(DeprecationWarning): | |||||
pass | |||||
class FixupWarning(UserWarning): | |||||
pass | |||||
class DuplicateNodenameWarning(UserWarning): | |||||
"""Multiple workers are using the same nodename.""" |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.five | |||||
~~~~~~~~~~~ | |||||
Compatibility implementations of features | |||||
only available in newer Python versions. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import io | |||||
import operator | |||||
import sys | |||||
from importlib import import_module | |||||
from types import ModuleType | |||||
from kombu.five import monotonic | |||||
try: | |||||
from collections import Counter | |||||
except ImportError: # pragma: no cover | |||||
from collections import defaultdict | |||||
def Counter(): # noqa | |||||
return defaultdict(int) | |||||
__all__ = ['Counter', 'reload', 'UserList', 'UserDict', 'Queue', 'Empty', | |||||
'zip_longest', 'map', 'string', 'string_t', | |||||
'long_t', 'text_t', 'range', 'int_types', 'items', 'keys', 'values', | |||||
'nextfun', 'reraise', 'WhateverIO', 'with_metaclass', | |||||
'OrderedDict', 'THREAD_TIMEOUT_MAX', 'format_d', | |||||
'class_property', 'reclassmethod', 'create_module', | |||||
'recreate_module', 'monotonic'] | |||||
# ############# py3k ######################################################### | |||||
PY3 = sys.version_info[0] == 3 | |||||
try: | |||||
reload = reload # noqa | |||||
except NameError: # pragma: no cover | |||||
from imp import reload # noqa | |||||
try: | |||||
from UserList import UserList # noqa | |||||
except ImportError: # pragma: no cover | |||||
from collections import UserList # noqa | |||||
try: | |||||
from UserDict import UserDict # noqa | |||||
except ImportError: # pragma: no cover | |||||
from collections import UserDict # noqa | |||||
if PY3: # pragma: no cover | |||||
import builtins | |||||
from queue import Queue, Empty | |||||
from itertools import zip_longest | |||||
map = map | |||||
string = str | |||||
string_t = str | |||||
long_t = int | |||||
text_t = str | |||||
range = range | |||||
int_types = (int, ) | |||||
_byte_t = bytes | |||||
open_fqdn = 'builtins.open' | |||||
def items(d): | |||||
return d.items() | |||||
def keys(d): | |||||
return d.keys() | |||||
def values(d): | |||||
return d.values() | |||||
def nextfun(it): | |||||
return it.__next__ | |||||
exec_ = getattr(builtins, 'exec') | |||||
def reraise(tp, value, tb=None): | |||||
if value.__traceback__ is not tb: | |||||
raise value.with_traceback(tb) | |||||
raise value | |||||
else: | |||||
import __builtin__ as builtins # noqa | |||||
from Queue import Queue, Empty # noqa | |||||
from itertools import imap as map, izip_longest as zip_longest # noqa | |||||
string = unicode # noqa | |||||
string_t = basestring # noqa | |||||
text_t = unicode # noqa | |||||
long_t = long # noqa | |||||
range = xrange # noqa | |||||
int_types = (int, long) # noqa | |||||
_byte_t = (str, bytes) # noqa | |||||
open_fqdn = '__builtin__.open' | |||||
def items(d): # noqa | |||||
return d.iteritems() | |||||
def keys(d): # noqa | |||||
return d.iterkeys() | |||||
def values(d): # noqa | |||||
return d.itervalues() | |||||
def nextfun(it): # noqa | |||||
return it.next | |||||
def exec_(code, globs=None, locs=None): # pragma: no cover | |||||
"""Execute code in a namespace.""" | |||||
if globs is None: | |||||
frame = sys._getframe(1) | |||||
globs = frame.f_globals | |||||
if locs is None: | |||||
locs = frame.f_locals | |||||
del frame | |||||
elif locs is None: | |||||
locs = globs | |||||
exec("""exec code in globs, locs""") | |||||
exec_("""def reraise(tp, value, tb=None): raise tp, value, tb""") | |||||
def with_metaclass(Type, skip_attrs=set(['__dict__', '__weakref__'])): | |||||
"""Class decorator to set metaclass. | |||||
Works with both Python 2 and Python 3 and it does not add | |||||
an extra class in the lookup order like ``six.with_metaclass`` does | |||||
(that is -- it copies the original class instead of using inheritance). | |||||
""" | |||||
def _clone_with_metaclass(Class): | |||||
attrs = dict((key, value) for key, value in items(vars(Class)) | |||||
if key not in skip_attrs) | |||||
return Type(Class.__name__, Class.__bases__, attrs) | |||||
return _clone_with_metaclass | |||||
# ############# collections.OrderedDict ###################################### | |||||
# was moved to kombu | |||||
from kombu.utils.compat import OrderedDict # noqa | |||||
# ############# threading.TIMEOUT_MAX ######################################## | |||||
try: | |||||
from threading import TIMEOUT_MAX as THREAD_TIMEOUT_MAX | |||||
except ImportError: | |||||
THREAD_TIMEOUT_MAX = 1e10 # noqa | |||||
# ############# format(int, ',d') ############################################ | |||||
if sys.version_info >= (2, 7): # pragma: no cover | |||||
def format_d(i): | |||||
return format(i, ',d') | |||||
else: # pragma: no cover | |||||
def format_d(i): # noqa | |||||
s = '%d' % i | |||||
groups = [] | |||||
while s and s[-1].isdigit(): | |||||
groups.append(s[-3:]) | |||||
s = s[:-3] | |||||
return s + ','.join(reversed(groups)) | |||||
# ############# Module Generation ############################################ | |||||
# Utilities to dynamically | |||||
# recreate modules, either for lazy loading or | |||||
# to create old modules at runtime instead of | |||||
# having them litter the source tree. | |||||
# import fails in python 2.5. fallback to reduce in stdlib | |||||
try: | |||||
from functools import reduce | |||||
except ImportError: | |||||
pass | |||||
MODULE_DEPRECATED = """ | |||||
The module %s is deprecated and will be removed in a future version. | |||||
""" | |||||
DEFAULT_ATTRS = set(['__file__', '__path__', '__doc__', '__all__']) | |||||
# im_func is no longer available in Py3. | |||||
# instead the unbound method itself can be used. | |||||
if sys.version_info[0] == 3: # pragma: no cover | |||||
def fun_of_method(method): | |||||
return method | |||||
else: | |||||
def fun_of_method(method): # noqa | |||||
return method.im_func | |||||
def getappattr(path): | |||||
"""Gets attribute from the current_app recursively, | |||||
e.g. getappattr('amqp.get_task_consumer')``.""" | |||||
from celery import current_app | |||||
return current_app._rgetattr(path) | |||||
def _compat_task_decorator(*args, **kwargs): | |||||
from celery import current_app | |||||
kwargs.setdefault('accept_magic_kwargs', True) | |||||
return current_app.task(*args, **kwargs) | |||||
def _compat_periodic_task_decorator(*args, **kwargs): | |||||
from celery.task import periodic_task | |||||
kwargs.setdefault('accept_magic_kwargs', True) | |||||
return periodic_task(*args, **kwargs) | |||||
COMPAT_MODULES = { | |||||
'celery': { | |||||
'execute': { | |||||
'send_task': 'send_task', | |||||
}, | |||||
'decorators': { | |||||
'task': _compat_task_decorator, | |||||
'periodic_task': _compat_periodic_task_decorator, | |||||
}, | |||||
'log': { | |||||
'get_default_logger': 'log.get_default_logger', | |||||
'setup_logger': 'log.setup_logger', | |||||
'setup_logging_subsystem': 'log.setup_logging_subsystem', | |||||
'redirect_stdouts_to_logger': 'log.redirect_stdouts_to_logger', | |||||
}, | |||||
'messaging': { | |||||
'TaskPublisher': 'amqp.TaskPublisher', | |||||
'TaskConsumer': 'amqp.TaskConsumer', | |||||
'establish_connection': 'connection', | |||||
'get_consumer_set': 'amqp.TaskConsumer', | |||||
}, | |||||
'registry': { | |||||
'tasks': 'tasks', | |||||
}, | |||||
}, | |||||
'celery.task': { | |||||
'control': { | |||||
'broadcast': 'control.broadcast', | |||||
'rate_limit': 'control.rate_limit', | |||||
'time_limit': 'control.time_limit', | |||||
'ping': 'control.ping', | |||||
'revoke': 'control.revoke', | |||||
'discard_all': 'control.purge', | |||||
'inspect': 'control.inspect', | |||||
}, | |||||
'schedules': 'celery.schedules', | |||||
'chords': 'celery.canvas', | |||||
} | |||||
} | |||||
class class_property(object): | |||||
def __init__(self, getter=None, setter=None): | |||||
if getter is not None and not isinstance(getter, classmethod): | |||||
getter = classmethod(getter) | |||||
if setter is not None and not isinstance(setter, classmethod): | |||||
setter = classmethod(setter) | |||||
self.__get = getter | |||||
self.__set = setter | |||||
info = getter.__get__(object) # just need the info attrs. | |||||
self.__doc__ = info.__doc__ | |||||
self.__name__ = info.__name__ | |||||
self.__module__ = info.__module__ | |||||
def __get__(self, obj, type=None): | |||||
if obj and type is None: | |||||
type = obj.__class__ | |||||
return self.__get.__get__(obj, type)() | |||||
def __set__(self, obj, value): | |||||
if obj is None: | |||||
return self | |||||
return self.__set.__get__(obj)(value) | |||||
def setter(self, setter): | |||||
return self.__class__(self.__get, setter) | |||||
def reclassmethod(method): | |||||
return classmethod(fun_of_method(method)) | |||||
class LazyModule(ModuleType): | |||||
_compat_modules = () | |||||
_all_by_module = {} | |||||
_direct = {} | |||||
_object_origins = {} | |||||
def __getattr__(self, name): | |||||
if name in self._object_origins: | |||||
module = __import__(self._object_origins[name], None, None, [name]) | |||||
for item in self._all_by_module[module.__name__]: | |||||
setattr(self, item, getattr(module, item)) | |||||
return getattr(module, name) | |||||
elif name in self._direct: # pragma: no cover | |||||
module = __import__(self._direct[name], None, None, [name]) | |||||
setattr(self, name, module) | |||||
return module | |||||
return ModuleType.__getattribute__(self, name) | |||||
def __dir__(self): | |||||
return list(set(self.__all__) | DEFAULT_ATTRS) | |||||
def __reduce__(self): | |||||
return import_module, (self.__name__, ) | |||||
def create_module(name, attrs, cls_attrs=None, pkg=None, | |||||
base=LazyModule, prepare_attr=None): | |||||
fqdn = '.'.join([pkg.__name__, name]) if pkg else name | |||||
cls_attrs = {} if cls_attrs is None else cls_attrs | |||||
pkg, _, modname = name.rpartition('.') | |||||
cls_attrs['__module__'] = pkg | |||||
attrs = dict((attr_name, prepare_attr(attr) if prepare_attr else attr) | |||||
for attr_name, attr in items(attrs)) | |||||
module = sys.modules[fqdn] = type(modname, (base, ), cls_attrs)(fqdn) | |||||
module.__dict__.update(attrs) | |||||
return module | |||||
def recreate_module(name, compat_modules=(), by_module={}, direct={}, | |||||
base=LazyModule, **attrs): | |||||
old_module = sys.modules[name] | |||||
origins = get_origins(by_module) | |||||
compat_modules = COMPAT_MODULES.get(name, ()) | |||||
cattrs = dict( | |||||
_compat_modules=compat_modules, | |||||
_all_by_module=by_module, _direct=direct, | |||||
_object_origins=origins, | |||||
__all__=tuple(set(reduce( | |||||
operator.add, | |||||
[tuple(v) for v in [compat_modules, origins, direct, attrs]], | |||||
))), | |||||
) | |||||
new_module = create_module(name, attrs, cls_attrs=cattrs, base=base) | |||||
new_module.__dict__.update(dict((mod, get_compat_module(new_module, mod)) | |||||
for mod in compat_modules)) | |||||
return old_module, new_module | |||||
def get_compat_module(pkg, name): | |||||
from .local import Proxy | |||||
def prepare(attr): | |||||
if isinstance(attr, string_t): | |||||
return Proxy(getappattr, (attr, )) | |||||
return attr | |||||
attrs = COMPAT_MODULES[pkg.__name__][name] | |||||
if isinstance(attrs, string_t): | |||||
fqdn = '.'.join([pkg.__name__, name]) | |||||
module = sys.modules[fqdn] = import_module(attrs) | |||||
return module | |||||
attrs['__all__'] = list(attrs) | |||||
return create_module(name, dict(attrs), pkg=pkg, prepare_attr=prepare) | |||||
def get_origins(defs): | |||||
origins = {} | |||||
for module, attrs in items(defs): | |||||
origins.update(dict((attr, module) for attr in attrs)) | |||||
return origins | |||||
_SIO_write = io.StringIO.write | |||||
_SIO_init = io.StringIO.__init__ | |||||
class WhateverIO(io.StringIO): | |||||
def __init__(self, v=None, *a, **kw): | |||||
_SIO_init(self, v.decode() if isinstance(v, _byte_t) else v, *a, **kw) | |||||
def write(self, data): | |||||
_SIO_write(self, data.decode() if isinstance(data, _byte_t) else data) |
from __future__ import absolute_import | |||||
import os | |||||
import sys | |||||
import warnings | |||||
from kombu.utils import cached_property, symbol_by_name | |||||
from datetime import datetime | |||||
from importlib import import_module | |||||
from celery import signals | |||||
from celery.exceptions import FixupWarning | |||||
if sys.version_info[0] < 3 and not hasattr(sys, 'pypy_version_info'): | |||||
from StringIO import StringIO | |||||
else: | |||||
from io import StringIO | |||||
__all__ = ['DjangoFixup', 'fixup'] | |||||
ERR_NOT_INSTALLED = """\ | |||||
Environment variable DJANGO_SETTINGS_MODULE is defined | |||||
but Django is not installed. Will not apply Django fixups! | |||||
""" | |||||
def _maybe_close_fd(fh): | |||||
try: | |||||
os.close(fh.fileno()) | |||||
except (AttributeError, OSError, TypeError): | |||||
# TypeError added for celery#962 | |||||
pass | |||||
def fixup(app, env='DJANGO_SETTINGS_MODULE'): | |||||
SETTINGS_MODULE = os.environ.get(env) | |||||
if SETTINGS_MODULE and 'django' not in app.loader_cls.lower(): | |||||
try: | |||||
import django # noqa | |||||
except ImportError: | |||||
warnings.warn(FixupWarning(ERR_NOT_INSTALLED)) | |||||
else: | |||||
return DjangoFixup(app).install() | |||||
class DjangoFixup(object): | |||||
def __init__(self, app): | |||||
self.app = app | |||||
self.app.set_default() | |||||
self._worker_fixup = None | |||||
def install(self): | |||||
# Need to add project directory to path | |||||
sys.path.append(os.getcwd()) | |||||
self.app.loader.now = self.now | |||||
self.app.loader.mail_admins = self.mail_admins | |||||
signals.import_modules.connect(self.on_import_modules) | |||||
signals.worker_init.connect(self.on_worker_init) | |||||
return self | |||||
@cached_property | |||||
def worker_fixup(self): | |||||
if self._worker_fixup is None: | |||||
self._worker_fixup = DjangoWorkerFixup(self.app) | |||||
return self._worker_fixup | |||||
def on_import_modules(self, **kwargs): | |||||
# call django.setup() before task modules are imported | |||||
self.worker_fixup.validate_models() | |||||
def on_worker_init(self, **kwargs): | |||||
self.worker_fixup.install() | |||||
def now(self, utc=False): | |||||
return datetime.utcnow() if utc else self._now() | |||||
def mail_admins(self, subject, body, fail_silently=False, **kwargs): | |||||
return self._mail_admins(subject, body, fail_silently=fail_silently) | |||||
@cached_property | |||||
def _mail_admins(self): | |||||
return symbol_by_name('django.core.mail:mail_admins') | |||||
@cached_property | |||||
def _now(self): | |||||
try: | |||||
return symbol_by_name('django.utils.timezone:now') | |||||
except (AttributeError, ImportError): # pre django-1.4 | |||||
return datetime.now | |||||
class DjangoWorkerFixup(object): | |||||
_db_recycles = 0 | |||||
def __init__(self, app): | |||||
self.app = app | |||||
self.db_reuse_max = self.app.conf.get('CELERY_DB_REUSE_MAX', None) | |||||
self._db = import_module('django.db') | |||||
self._cache = import_module('django.core.cache') | |||||
self._settings = symbol_by_name('django.conf:settings') | |||||
# Database-related exceptions. | |||||
DatabaseError = symbol_by_name('django.db:DatabaseError') | |||||
try: | |||||
import MySQLdb as mysql | |||||
_my_database_errors = (mysql.DatabaseError, | |||||
mysql.InterfaceError, | |||||
mysql.OperationalError) | |||||
except ImportError: | |||||
_my_database_errors = () # noqa | |||||
try: | |||||
import psycopg2 as pg | |||||
_pg_database_errors = (pg.DatabaseError, | |||||
pg.InterfaceError, | |||||
pg.OperationalError) | |||||
except ImportError: | |||||
_pg_database_errors = () # noqa | |||||
try: | |||||
import sqlite3 | |||||
_lite_database_errors = (sqlite3.DatabaseError, | |||||
sqlite3.InterfaceError, | |||||
sqlite3.OperationalError) | |||||
except ImportError: | |||||
_lite_database_errors = () # noqa | |||||
try: | |||||
import cx_Oracle as oracle | |||||
_oracle_database_errors = (oracle.DatabaseError, | |||||
oracle.InterfaceError, | |||||
oracle.OperationalError) | |||||
except ImportError: | |||||
_oracle_database_errors = () # noqa | |||||
try: | |||||
self._close_old_connections = symbol_by_name( | |||||
'django.db:close_old_connections', | |||||
) | |||||
except (ImportError, AttributeError): | |||||
self._close_old_connections = None | |||||
self.database_errors = ( | |||||
(DatabaseError, ) + | |||||
_my_database_errors + | |||||
_pg_database_errors + | |||||
_lite_database_errors + | |||||
_oracle_database_errors | |||||
) | |||||
def validate_models(self): | |||||
import django | |||||
try: | |||||
django_setup = django.setup | |||||
except AttributeError: | |||||
pass | |||||
else: | |||||
django_setup() | |||||
s = StringIO() | |||||
try: | |||||
from django.core.management.validation import get_validation_errors | |||||
except ImportError: | |||||
from django.core.management.base import BaseCommand | |||||
cmd = BaseCommand() | |||||
try: | |||||
# since django 1.5 | |||||
from django.core.management.base import OutputWrapper | |||||
cmd.stdout = OutputWrapper(sys.stdout) | |||||
cmd.stderr = OutputWrapper(sys.stderr) | |||||
except ImportError: | |||||
cmd.stdout, cmd.stderr = sys.stdout, sys.stderr | |||||
cmd.check() | |||||
else: | |||||
num_errors = get_validation_errors(s, None) | |||||
if num_errors: | |||||
raise RuntimeError( | |||||
'One or more Django models did not validate:\n{0}'.format( | |||||
s.getvalue())) | |||||
def install(self): | |||||
signals.beat_embedded_init.connect(self.close_database) | |||||
signals.worker_ready.connect(self.on_worker_ready) | |||||
signals.task_prerun.connect(self.on_task_prerun) | |||||
signals.task_postrun.connect(self.on_task_postrun) | |||||
signals.worker_process_init.connect(self.on_worker_process_init) | |||||
self.close_database() | |||||
self.close_cache() | |||||
return self | |||||
def on_worker_process_init(self, **kwargs): | |||||
# Child process must validate models again if on Windows, | |||||
# or if they were started using execv. | |||||
if os.environ.get('FORKED_BY_MULTIPROCESSING'): | |||||
self.validate_models() | |||||
# close connections: | |||||
# the parent process may have established these, | |||||
# so need to close them. | |||||
# calling db.close() on some DB connections will cause | |||||
# the inherited DB conn to also get broken in the parent | |||||
# process so we need to remove it without triggering any | |||||
# network IO that close() might cause. | |||||
try: | |||||
for c in self._db.connections.all(): | |||||
if c and c.connection: | |||||
_maybe_close_fd(c.connection) | |||||
except AttributeError: | |||||
if self._db.connection and self._db.connection.connection: | |||||
_maybe_close_fd(self._db.connection.connection) | |||||
# use the _ version to avoid DB_REUSE preventing the conn.close() call | |||||
self._close_database() | |||||
self.close_cache() | |||||
def on_task_prerun(self, sender, **kwargs): | |||||
"""Called before every task.""" | |||||
if not getattr(sender.request, 'is_eager', False): | |||||
self.close_database() | |||||
def on_task_postrun(self, sender, **kwargs): | |||||
# See http://groups.google.com/group/django-users/ | |||||
# browse_thread/thread/78200863d0c07c6d/ | |||||
if not getattr(sender.request, 'is_eager', False): | |||||
self.close_database() | |||||
self.close_cache() | |||||
def close_database(self, **kwargs): | |||||
if self._close_old_connections: | |||||
return self._close_old_connections() # Django 1.6 | |||||
if not self.db_reuse_max: | |||||
return self._close_database() | |||||
if self._db_recycles >= self.db_reuse_max * 2: | |||||
self._db_recycles = 0 | |||||
self._close_database() | |||||
self._db_recycles += 1 | |||||
def _close_database(self): | |||||
try: | |||||
funs = [conn.close for conn in self._db.connections.all()] | |||||
except AttributeError: | |||||
if hasattr(self._db, 'close_old_connections'): # django 1.6 | |||||
funs = [self._db.close_old_connections] | |||||
else: | |||||
# pre multidb, pending deprication in django 1.6 | |||||
funs = [self._db.close_connection] | |||||
for close in funs: | |||||
try: | |||||
close() | |||||
except self.database_errors as exc: | |||||
str_exc = str(exc) | |||||
if 'closed' not in str_exc and 'not connected' not in str_exc: | |||||
raise | |||||
def close_cache(self): | |||||
try: | |||||
self._cache.cache.close() | |||||
except (TypeError, AttributeError): | |||||
pass | |||||
def on_worker_ready(self, **kwargs): | |||||
if self._settings.DEBUG: | |||||
warnings.warn('Using settings.DEBUG leads to a memory leak, never ' | |||||
'use this setting in production environments!') |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.loaders | |||||
~~~~~~~~~~~~~~ | |||||
Loaders define how configuration is read, what happens | |||||
when workers start, when tasks are executed and so on. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from celery._state import current_app | |||||
from celery.utils import deprecated | |||||
from celery.utils.imports import symbol_by_name, import_from_cwd | |||||
__all__ = ['get_loader_cls'] | |||||
LOADER_ALIASES = {'app': 'celery.loaders.app:AppLoader', | |||||
'default': 'celery.loaders.default:Loader', | |||||
'django': 'djcelery.loaders:DjangoLoader'} | |||||
def get_loader_cls(loader): | |||||
"""Get loader class by name/alias""" | |||||
return symbol_by_name(loader, LOADER_ALIASES, imp=import_from_cwd) | |||||
@deprecated(deprecation=2.5, removal=4.0, | |||||
alternative='celery.current_app.loader') | |||||
def current_loader(): | |||||
return current_app.loader | |||||
@deprecated(deprecation=2.5, removal=4.0, | |||||
alternative='celery.current_app.conf') | |||||
def load_settings(): | |||||
return current_app.conf |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.loaders.app | |||||
~~~~~~~~~~~~~~~~~~ | |||||
The default loader used with custom app instances. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from .base import BaseLoader | |||||
__all__ = ['AppLoader'] | |||||
class AppLoader(BaseLoader): | |||||
pass |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.loaders.base | |||||
~~~~~~~~~~~~~~~~~~~ | |||||
Loader base class. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import anyjson | |||||
import imp as _imp | |||||
import importlib | |||||
import os | |||||
import re | |||||
import sys | |||||
from datetime import datetime | |||||
from kombu.utils import cached_property | |||||
from kombu.utils.encoding import safe_str | |||||
from celery import signals | |||||
from celery.datastructures import DictAttribute, force_mapping | |||||
from celery.five import reraise, string_t | |||||
from celery.utils.functional import maybe_list | |||||
from celery.utils.imports import ( | |||||
import_from_cwd, symbol_by_name, NotAPackage, find_module, | |||||
) | |||||
__all__ = ['BaseLoader'] | |||||
_RACE_PROTECTION = False | |||||
CONFIG_INVALID_NAME = """\ | |||||
Error: Module '{module}' doesn't exist, or it's not a valid \ | |||||
Python module name. | |||||
""" | |||||
CONFIG_WITH_SUFFIX = CONFIG_INVALID_NAME + """\ | |||||
Did you mean '{suggest}'? | |||||
""" | |||||
class BaseLoader(object): | |||||
"""The base class for loaders. | |||||
Loaders handles, | |||||
* Reading celery client/worker configurations. | |||||
* What happens when a task starts? | |||||
See :meth:`on_task_init`. | |||||
* What happens when the worker starts? | |||||
See :meth:`on_worker_init`. | |||||
* What happens when the worker shuts down? | |||||
See :meth:`on_worker_shutdown`. | |||||
* What modules are imported to find tasks? | |||||
""" | |||||
builtin_modules = frozenset() | |||||
configured = False | |||||
override_backends = {} | |||||
worker_initialized = False | |||||
_conf = None | |||||
def __init__(self, app, **kwargs): | |||||
self.app = app | |||||
self.task_modules = set() | |||||
def now(self, utc=True): | |||||
if utc: | |||||
return datetime.utcnow() | |||||
return datetime.now() | |||||
def on_task_init(self, task_id, task): | |||||
"""This method is called before a task is executed.""" | |||||
pass | |||||
def on_process_cleanup(self): | |||||
"""This method is called after a task is executed.""" | |||||
pass | |||||
def on_worker_init(self): | |||||
"""This method is called when the worker (:program:`celery worker`) | |||||
starts.""" | |||||
pass | |||||
def on_worker_shutdown(self): | |||||
"""This method is called when the worker (:program:`celery worker`) | |||||
shuts down.""" | |||||
pass | |||||
def on_worker_process_init(self): | |||||
"""This method is called when a child process starts.""" | |||||
pass | |||||
def import_task_module(self, module): | |||||
self.task_modules.add(module) | |||||
return self.import_from_cwd(module) | |||||
def import_module(self, module, package=None): | |||||
return importlib.import_module(module, package=package) | |||||
def import_from_cwd(self, module, imp=None, package=None): | |||||
return import_from_cwd( | |||||
module, | |||||
self.import_module if imp is None else imp, | |||||
package=package, | |||||
) | |||||
def import_default_modules(self): | |||||
signals.import_modules.send(sender=self.app) | |||||
return [ | |||||
self.import_task_module(m) for m in ( | |||||
tuple(self.builtin_modules) + | |||||
tuple(maybe_list(self.app.conf.CELERY_IMPORTS)) + | |||||
tuple(maybe_list(self.app.conf.CELERY_INCLUDE)) | |||||
) | |||||
] | |||||
def init_worker(self): | |||||
if not self.worker_initialized: | |||||
self.worker_initialized = True | |||||
self.import_default_modules() | |||||
self.on_worker_init() | |||||
def shutdown_worker(self): | |||||
self.on_worker_shutdown() | |||||
def init_worker_process(self): | |||||
self.on_worker_process_init() | |||||
def config_from_object(self, obj, silent=False): | |||||
if isinstance(obj, string_t): | |||||
try: | |||||
obj = self._smart_import(obj, imp=self.import_from_cwd) | |||||
except (ImportError, AttributeError): | |||||
if silent: | |||||
return False | |||||
raise | |||||
self._conf = force_mapping(obj) | |||||
return True | |||||
def _smart_import(self, path, imp=None): | |||||
imp = self.import_module if imp is None else imp | |||||
if ':' in path: | |||||
# Path includes attribute so can just jump here. | |||||
# e.g. ``os.path:abspath``. | |||||
return symbol_by_name(path, imp=imp) | |||||
# Not sure if path is just a module name or if it includes an | |||||
# attribute name (e.g. ``os.path``, vs, ``os.path.abspath``). | |||||
try: | |||||
return imp(path) | |||||
except ImportError: | |||||
# Not a module name, so try module + attribute. | |||||
return symbol_by_name(path, imp=imp) | |||||
def _import_config_module(self, name): | |||||
try: | |||||
self.find_module(name) | |||||
except NotAPackage: | |||||
if name.endswith('.py'): | |||||
reraise(NotAPackage, NotAPackage(CONFIG_WITH_SUFFIX.format( | |||||
module=name, suggest=name[:-3])), sys.exc_info()[2]) | |||||
reraise(NotAPackage, NotAPackage(CONFIG_INVALID_NAME.format( | |||||
module=name)), sys.exc_info()[2]) | |||||
else: | |||||
return self.import_from_cwd(name) | |||||
def find_module(self, module): | |||||
return find_module(module) | |||||
def cmdline_config_parser( | |||||
self, args, namespace='celery', | |||||
re_type=re.compile(r'\((\w+)\)'), | |||||
extra_types={'json': anyjson.loads}, | |||||
override_types={'tuple': 'json', | |||||
'list': 'json', | |||||
'dict': 'json'}): | |||||
from celery.app.defaults import Option, NAMESPACES | |||||
namespace = namespace.upper() | |||||
typemap = dict(Option.typemap, **extra_types) | |||||
def getarg(arg): | |||||
"""Parse a single configuration definition from | |||||
the command-line.""" | |||||
# ## find key/value | |||||
# ns.key=value|ns_key=value (case insensitive) | |||||
key, value = arg.split('=', 1) | |||||
key = key.upper().replace('.', '_') | |||||
# ## find namespace. | |||||
# .key=value|_key=value expands to default namespace. | |||||
if key[0] == '_': | |||||
ns, key = namespace, key[1:] | |||||
else: | |||||
# find namespace part of key | |||||
ns, key = key.split('_', 1) | |||||
ns_key = (ns and ns + '_' or '') + key | |||||
# (type)value makes cast to custom type. | |||||
cast = re_type.match(value) | |||||
if cast: | |||||
type_ = cast.groups()[0] | |||||
type_ = override_types.get(type_, type_) | |||||
value = value[len(cast.group()):] | |||||
value = typemap[type_](value) | |||||
else: | |||||
try: | |||||
value = NAMESPACES[ns][key].to_python(value) | |||||
except ValueError as exc: | |||||
# display key name in error message. | |||||
raise ValueError('{0!r}: {1}'.format(ns_key, exc)) | |||||
return ns_key, value | |||||
return dict(getarg(arg) for arg in args) | |||||
def mail_admins(self, subject, body, fail_silently=False, | |||||
sender=None, to=None, host=None, port=None, | |||||
user=None, password=None, timeout=None, | |||||
use_ssl=False, use_tls=False, charset='utf-8'): | |||||
message = self.mail.Message(sender=sender, to=to, | |||||
subject=safe_str(subject), | |||||
body=safe_str(body), | |||||
charset=charset) | |||||
mailer = self.mail.Mailer(host=host, port=port, | |||||
user=user, password=password, | |||||
timeout=timeout, use_ssl=use_ssl, | |||||
use_tls=use_tls) | |||||
mailer.send(message, fail_silently=fail_silently) | |||||
def read_configuration(self, env='CELERY_CONFIG_MODULE'): | |||||
try: | |||||
custom_config = os.environ[env] | |||||
except KeyError: | |||||
pass | |||||
else: | |||||
if custom_config: | |||||
usercfg = self._import_config_module(custom_config) | |||||
return DictAttribute(usercfg) | |||||
return {} | |||||
def autodiscover_tasks(self, packages, related_name='tasks'): | |||||
self.task_modules.update( | |||||
mod.__name__ for mod in autodiscover_tasks(packages or (), | |||||
related_name) if mod) | |||||
@property | |||||
def conf(self): | |||||
"""Loader configuration.""" | |||||
if self._conf is None: | |||||
self._conf = self.read_configuration() | |||||
return self._conf | |||||
@cached_property | |||||
def mail(self): | |||||
return self.import_module('celery.utils.mail') | |||||
def autodiscover_tasks(packages, related_name='tasks'): | |||||
global _RACE_PROTECTION | |||||
if _RACE_PROTECTION: | |||||
return () | |||||
_RACE_PROTECTION = True | |||||
try: | |||||
return [find_related_module(pkg, related_name) for pkg in packages] | |||||
finally: | |||||
_RACE_PROTECTION = False | |||||
def find_related_module(package, related_name): | |||||
"""Given a package name and a module name, tries to find that | |||||
module.""" | |||||
# Django 1.7 allows for speciying a class name in INSTALLED_APPS. | |||||
# (Issue #2248). | |||||
try: | |||||
importlib.import_module(package) | |||||
except ImportError: | |||||
package, _, _ = package.rpartition('.') | |||||
try: | |||||
pkg_path = importlib.import_module(package).__path__ | |||||
except AttributeError: | |||||
return | |||||
try: | |||||
_imp.find_module(related_name, pkg_path) | |||||
except ImportError: | |||||
return | |||||
return importlib.import_module('{0}.{1}'.format(package, related_name)) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.loaders.default | |||||
~~~~~~~~~~~~~~~~~~~~~~ | |||||
The default loader used when no custom app has been initialized. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import os | |||||
import warnings | |||||
from celery.datastructures import DictAttribute | |||||
from celery.exceptions import NotConfigured | |||||
from celery.utils import strtobool | |||||
from .base import BaseLoader | |||||
__all__ = ['Loader', 'DEFAULT_CONFIG_MODULE'] | |||||
DEFAULT_CONFIG_MODULE = 'celeryconfig' | |||||
#: Warns if configuration file is missing if :envvar:`C_WNOCONF` is set. | |||||
C_WNOCONF = strtobool(os.environ.get('C_WNOCONF', False)) | |||||
class Loader(BaseLoader): | |||||
"""The loader used by the default app.""" | |||||
def setup_settings(self, settingsdict): | |||||
return DictAttribute(settingsdict) | |||||
def read_configuration(self, fail_silently=True): | |||||
"""Read configuration from :file:`celeryconfig.py` and configure | |||||
celery and Django so it can be used by regular Python.""" | |||||
configname = os.environ.get('CELERY_CONFIG_MODULE', | |||||
DEFAULT_CONFIG_MODULE) | |||||
try: | |||||
usercfg = self._import_config_module(configname) | |||||
except ImportError: | |||||
if not fail_silently: | |||||
raise | |||||
# billiard sets this if forked using execv | |||||
if C_WNOCONF and not os.environ.get('FORKED_BY_MULTIPROCESSING'): | |||||
warnings.warn(NotConfigured( | |||||
'No {module} module found! Please make sure it exists and ' | |||||
'is available to Python.'.format(module=configname))) | |||||
return self.setup_settings({}) | |||||
else: | |||||
self.configured = True | |||||
return self.setup_settings(usercfg) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.local | |||||
~~~~~~~~~~~~ | |||||
This module contains critical utilities that | |||||
needs to be loaded as soon as possible, and that | |||||
shall not load any third party modules. | |||||
Parts of this module is Copyright by Werkzeug Team. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import importlib | |||||
import sys | |||||
from .five import string | |||||
__all__ = ['Proxy', 'PromiseProxy', 'try_import', 'maybe_evaluate'] | |||||
__module__ = __name__ # used by Proxy class body | |||||
PY3 = sys.version_info[0] == 3 | |||||
def _default_cls_attr(name, type_, cls_value): | |||||
# Proxy uses properties to forward the standard | |||||
# class attributes __module__, __name__ and __doc__ to the real | |||||
# object, but these needs to be a string when accessed from | |||||
# the Proxy class directly. This is a hack to make that work. | |||||
# -- See Issue #1087. | |||||
def __new__(cls, getter): | |||||
instance = type_.__new__(cls, cls_value) | |||||
instance.__getter = getter | |||||
return instance | |||||
def __get__(self, obj, cls=None): | |||||
return self.__getter(obj) if obj is not None else self | |||||
return type(name, (type_, ), { | |||||
'__new__': __new__, '__get__': __get__, | |||||
}) | |||||
def try_import(module, default=None): | |||||
"""Try to import and return module, or return | |||||
None if the module does not exist.""" | |||||
try: | |||||
return importlib.import_module(module) | |||||
except ImportError: | |||||
return default | |||||
class Proxy(object): | |||||
"""Proxy to another object.""" | |||||
# Code stolen from werkzeug.local.Proxy. | |||||
__slots__ = ('__local', '__args', '__kwargs', '__dict__') | |||||
def __init__(self, local, | |||||
args=None, kwargs=None, name=None, __doc__=None): | |||||
object.__setattr__(self, '_Proxy__local', local) | |||||
object.__setattr__(self, '_Proxy__args', args or ()) | |||||
object.__setattr__(self, '_Proxy__kwargs', kwargs or {}) | |||||
if name is not None: | |||||
object.__setattr__(self, '__custom_name__', name) | |||||
if __doc__ is not None: | |||||
object.__setattr__(self, '__doc__', __doc__) | |||||
@_default_cls_attr('name', str, __name__) | |||||
def __name__(self): | |||||
try: | |||||
return self.__custom_name__ | |||||
except AttributeError: | |||||
return self._get_current_object().__name__ | |||||
@_default_cls_attr('module', str, __module__) | |||||
def __module__(self): | |||||
return self._get_current_object().__module__ | |||||
@_default_cls_attr('doc', str, __doc__) | |||||
def __doc__(self): | |||||
return self._get_current_object().__doc__ | |||||
def _get_class(self): | |||||
return self._get_current_object().__class__ | |||||
@property | |||||
def __class__(self): | |||||
return self._get_class() | |||||
def _get_current_object(self): | |||||
"""Return the current object. This is useful if you want the real | |||||
object behind the proxy at a time for performance reasons or because | |||||
you want to pass the object into a different context. | |||||
""" | |||||
loc = object.__getattribute__(self, '_Proxy__local') | |||||
if not hasattr(loc, '__release_local__'): | |||||
return loc(*self.__args, **self.__kwargs) | |||||
try: | |||||
return getattr(loc, self.__name__) | |||||
except AttributeError: | |||||
raise RuntimeError('no object bound to {0.__name__}'.format(self)) | |||||
@property | |||||
def __dict__(self): | |||||
try: | |||||
return self._get_current_object().__dict__ | |||||
except RuntimeError: # pragma: no cover | |||||
raise AttributeError('__dict__') | |||||
def __repr__(self): | |||||
try: | |||||
obj = self._get_current_object() | |||||
except RuntimeError: # pragma: no cover | |||||
return '<{0} unbound>'.format(self.__class__.__name__) | |||||
return repr(obj) | |||||
def __bool__(self): | |||||
try: | |||||
return bool(self._get_current_object()) | |||||
except RuntimeError: # pragma: no cover | |||||
return False | |||||
__nonzero__ = __bool__ # Py2 | |||||
def __unicode__(self): | |||||
try: | |||||
return string(self._get_current_object()) | |||||
except RuntimeError: # pragma: no cover | |||||
return repr(self) | |||||
def __dir__(self): | |||||
try: | |||||
return dir(self._get_current_object()) | |||||
except RuntimeError: # pragma: no cover | |||||
return [] | |||||
def __getattr__(self, name): | |||||
if name == '__members__': | |||||
return dir(self._get_current_object()) | |||||
return getattr(self._get_current_object(), name) | |||||
def __setitem__(self, key, value): | |||||
self._get_current_object()[key] = value | |||||
def __delitem__(self, key): | |||||
del self._get_current_object()[key] | |||||
def __setslice__(self, i, j, seq): | |||||
self._get_current_object()[i:j] = seq | |||||
def __delslice__(self, i, j): | |||||
del self._get_current_object()[i:j] | |||||
def __setattr__(self, name, value): | |||||
setattr(self._get_current_object(), name, value) | |||||
def __delattr__(self, name): | |||||
delattr(self._get_current_object(), name) | |||||
def __str__(self): | |||||
return str(self._get_current_object()) | |||||
def __lt__(self, other): | |||||
return self._get_current_object() < other | |||||
def __le__(self, other): | |||||
return self._get_current_object() <= other | |||||
def __eq__(self, other): | |||||
return self._get_current_object() == other | |||||
def __ne__(self, other): | |||||
return self._get_current_object() != other | |||||
def __gt__(self, other): | |||||
return self._get_current_object() > other | |||||
def __ge__(self, other): | |||||
return self._get_current_object() >= other | |||||
def __hash__(self): | |||||
return hash(self._get_current_object()) | |||||
def __call__(self, *a, **kw): | |||||
return self._get_current_object()(*a, **kw) | |||||
def __len__(self): | |||||
return len(self._get_current_object()) | |||||
def __getitem__(self, i): | |||||
return self._get_current_object()[i] | |||||
def __iter__(self): | |||||
return iter(self._get_current_object()) | |||||
def __contains__(self, i): | |||||
return i in self._get_current_object() | |||||
def __getslice__(self, i, j): | |||||
return self._get_current_object()[i:j] | |||||
def __add__(self, other): | |||||
return self._get_current_object() + other | |||||
def __sub__(self, other): | |||||
return self._get_current_object() - other | |||||
def __mul__(self, other): | |||||
return self._get_current_object() * other | |||||
def __floordiv__(self, other): | |||||
return self._get_current_object() // other | |||||
def __mod__(self, other): | |||||
return self._get_current_object() % other | |||||
def __divmod__(self, other): | |||||
return self._get_current_object().__divmod__(other) | |||||
def __pow__(self, other): | |||||
return self._get_current_object() ** other | |||||
def __lshift__(self, other): | |||||
return self._get_current_object() << other | |||||
def __rshift__(self, other): | |||||
return self._get_current_object() >> other | |||||
def __and__(self, other): | |||||
return self._get_current_object() & other | |||||
def __xor__(self, other): | |||||
return self._get_current_object() ^ other | |||||
def __or__(self, other): | |||||
return self._get_current_object() | other | |||||
def __div__(self, other): | |||||
return self._get_current_object().__div__(other) | |||||
def __truediv__(self, other): | |||||
return self._get_current_object().__truediv__(other) | |||||
def __neg__(self): | |||||
return -(self._get_current_object()) | |||||
def __pos__(self): | |||||
return +(self._get_current_object()) | |||||
def __abs__(self): | |||||
return abs(self._get_current_object()) | |||||
def __invert__(self): | |||||
return ~(self._get_current_object()) | |||||
def __complex__(self): | |||||
return complex(self._get_current_object()) | |||||
def __int__(self): | |||||
return int(self._get_current_object()) | |||||
def __float__(self): | |||||
return float(self._get_current_object()) | |||||
def __oct__(self): | |||||
return oct(self._get_current_object()) | |||||
def __hex__(self): | |||||
return hex(self._get_current_object()) | |||||
def __index__(self): | |||||
return self._get_current_object().__index__() | |||||
def __coerce__(self, other): | |||||
return self._get_current_object().__coerce__(other) | |||||
def __enter__(self): | |||||
return self._get_current_object().__enter__() | |||||
def __exit__(self, *a, **kw): | |||||
return self._get_current_object().__exit__(*a, **kw) | |||||
def __reduce__(self): | |||||
return self._get_current_object().__reduce__() | |||||
if not PY3: | |||||
def __cmp__(self, other): | |||||
return cmp(self._get_current_object(), other) # noqa | |||||
def __long__(self): | |||||
return long(self._get_current_object()) # noqa | |||||
class PromiseProxy(Proxy): | |||||
"""This is a proxy to an object that has not yet been evaulated. | |||||
:class:`Proxy` will evaluate the object each time, while the | |||||
promise will only evaluate it once. | |||||
""" | |||||
__slots__ = ('__pending__', ) | |||||
def _get_current_object(self): | |||||
try: | |||||
return object.__getattribute__(self, '__thing') | |||||
except AttributeError: | |||||
return self.__evaluate__() | |||||
def __then__(self, fun, *args, **kwargs): | |||||
if self.__evaluated__(): | |||||
return fun(*args, **kwargs) | |||||
from collections import deque | |||||
try: | |||||
pending = object.__getattribute__(self, '__pending__') | |||||
except AttributeError: | |||||
pending = None | |||||
if pending is None: | |||||
pending = deque() | |||||
object.__setattr__(self, '__pending__', pending) | |||||
pending.append((fun, args, kwargs)) | |||||
def __evaluated__(self): | |||||
try: | |||||
object.__getattribute__(self, '__thing') | |||||
except AttributeError: | |||||
return False | |||||
return True | |||||
def __maybe_evaluate__(self): | |||||
return self._get_current_object() | |||||
def __evaluate__(self, | |||||
_clean=('_Proxy__local', | |||||
'_Proxy__args', | |||||
'_Proxy__kwargs')): | |||||
try: | |||||
thing = Proxy._get_current_object(self) | |||||
except: | |||||
raise | |||||
else: | |||||
object.__setattr__(self, '__thing', thing) | |||||
for attr in _clean: | |||||
try: | |||||
object.__delattr__(self, attr) | |||||
except AttributeError: # pragma: no cover | |||||
# May mask errors so ignore | |||||
pass | |||||
try: | |||||
pending = object.__getattribute__(self, '__pending__') | |||||
except AttributeError: | |||||
pass | |||||
else: | |||||
try: | |||||
while pending: | |||||
fun, args, kwargs = pending.popleft() | |||||
fun(*args, **kwargs) | |||||
finally: | |||||
try: | |||||
object.__delattr__(self, '__pending__') | |||||
except AttributeError: | |||||
pass | |||||
return thing | |||||
def maybe_evaluate(obj): | |||||
try: | |||||
return obj.__maybe_evaluate__() | |||||
except AttributeError: | |||||
return obj |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.platforms | |||||
~~~~~~~~~~~~~~~~ | |||||
Utilities dealing with platform specifics: signals, daemonization, | |||||
users, groups, and so on. | |||||
""" | |||||
from __future__ import absolute_import, print_function | |||||
import atexit | |||||
import errno | |||||
import math | |||||
import numbers | |||||
import os | |||||
import platform as _platform | |||||
import signal as _signal | |||||
import sys | |||||
import warnings | |||||
from collections import namedtuple | |||||
from billiard import current_process | |||||
# fileno used to be in this module | |||||
from kombu.utils import maybe_fileno | |||||
from kombu.utils.compat import get_errno | |||||
from kombu.utils.encoding import safe_str | |||||
from contextlib import contextmanager | |||||
from .local import try_import | |||||
from .five import items, range, reraise, string_t, zip_longest | |||||
from .utils.functional import uniq | |||||
_setproctitle = try_import('setproctitle') | |||||
resource = try_import('resource') | |||||
pwd = try_import('pwd') | |||||
grp = try_import('grp') | |||||
mputil = try_import('multiprocessing.util') | |||||
__all__ = ['EX_OK', 'EX_FAILURE', 'EX_UNAVAILABLE', 'EX_USAGE', 'SYSTEM', | |||||
'IS_OSX', 'IS_WINDOWS', 'pyimplementation', 'LockFailed', | |||||
'get_fdmax', 'Pidfile', 'create_pidlock', | |||||
'close_open_fds', 'DaemonContext', 'detached', 'parse_uid', | |||||
'parse_gid', 'setgroups', 'initgroups', 'setgid', 'setuid', | |||||
'maybe_drop_privileges', 'signals', 'set_process_title', | |||||
'set_mp_process_title', 'get_errno_name', 'ignore_errno', | |||||
'fd_by_path'] | |||||
# exitcodes | |||||
EX_OK = getattr(os, 'EX_OK', 0) | |||||
EX_FAILURE = 1 | |||||
EX_UNAVAILABLE = getattr(os, 'EX_UNAVAILABLE', 69) | |||||
EX_USAGE = getattr(os, 'EX_USAGE', 64) | |||||
EX_CANTCREAT = getattr(os, 'EX_CANTCREAT', 73) | |||||
SYSTEM = _platform.system() | |||||
IS_OSX = SYSTEM == 'Darwin' | |||||
IS_WINDOWS = SYSTEM == 'Windows' | |||||
DAEMON_WORKDIR = '/' | |||||
PIDFILE_FLAGS = os.O_CREAT | os.O_EXCL | os.O_WRONLY | |||||
PIDFILE_MODE = ((os.R_OK | os.W_OK) << 6) | ((os.R_OK) << 3) | ((os.R_OK)) | |||||
PIDLOCKED = """ERROR: Pidfile ({0}) already exists. | |||||
Seems we're already running? (pid: {1})""" | |||||
_range = namedtuple('_range', ('start', 'stop')) | |||||
C_FORCE_ROOT = os.environ.get('C_FORCE_ROOT', False) | |||||
ROOT_DISALLOWED = """\ | |||||
Running a worker with superuser privileges when the | |||||
worker accepts messages serialized with pickle is a very bad idea! | |||||
If you really want to continue then you have to set the C_FORCE_ROOT | |||||
environment variable (but please think about this before you do). | |||||
User information: uid={uid} euid={euid} gid={gid} egid={egid} | |||||
""" | |||||
ROOT_DISCOURAGED = """\ | |||||
You are running the worker with superuser privileges, which is | |||||
absolutely not recommended! | |||||
Please specify a different user using the -u option. | |||||
User information: uid={uid} euid={euid} gid={gid} egid={egid} | |||||
""" | |||||
def pyimplementation(): | |||||
"""Return string identifying the current Python implementation.""" | |||||
if hasattr(_platform, 'python_implementation'): | |||||
return _platform.python_implementation() | |||||
elif sys.platform.startswith('java'): | |||||
return 'Jython ' + sys.platform | |||||
elif hasattr(sys, 'pypy_version_info'): | |||||
v = '.'.join(str(p) for p in sys.pypy_version_info[:3]) | |||||
if sys.pypy_version_info[3:]: | |||||
v += '-' + ''.join(str(p) for p in sys.pypy_version_info[3:]) | |||||
return 'PyPy ' + v | |||||
else: | |||||
return 'CPython' | |||||
class LockFailed(Exception): | |||||
"""Raised if a pidlock can't be acquired.""" | |||||
def get_fdmax(default=None): | |||||
"""Return the maximum number of open file descriptors | |||||
on this system. | |||||
:keyword default: Value returned if there's no file | |||||
descriptor limit. | |||||
""" | |||||
try: | |||||
return os.sysconf('SC_OPEN_MAX') | |||||
except: | |||||
pass | |||||
if resource is None: # Windows | |||||
return default | |||||
fdmax = resource.getrlimit(resource.RLIMIT_NOFILE)[1] | |||||
if fdmax == resource.RLIM_INFINITY: | |||||
return default | |||||
return fdmax | |||||
class Pidfile(object): | |||||
"""Pidfile | |||||
This is the type returned by :func:`create_pidlock`. | |||||
TIP: Use the :func:`create_pidlock` function instead, | |||||
which is more convenient and also removes stale pidfiles (when | |||||
the process holding the lock is no longer running). | |||||
""" | |||||
#: Path to the pid lock file. | |||||
path = None | |||||
def __init__(self, path): | |||||
self.path = os.path.abspath(path) | |||||
def acquire(self): | |||||
"""Acquire lock.""" | |||||
try: | |||||
self.write_pid() | |||||
except OSError as exc: | |||||
reraise(LockFailed, LockFailed(str(exc)), sys.exc_info()[2]) | |||||
return self | |||||
__enter__ = acquire | |||||
def is_locked(self): | |||||
"""Return true if the pid lock exists.""" | |||||
return os.path.exists(self.path) | |||||
def release(self, *args): | |||||
"""Release lock.""" | |||||
self.remove() | |||||
__exit__ = release | |||||
def read_pid(self): | |||||
"""Read and return the current pid.""" | |||||
with ignore_errno('ENOENT'): | |||||
with open(self.path, 'r') as fh: | |||||
line = fh.readline() | |||||
if line.strip() == line: # must contain '\n' | |||||
raise ValueError( | |||||
'Partial or invalid pidfile {0.path}'.format(self)) | |||||
try: | |||||
return int(line.strip()) | |||||
except ValueError: | |||||
raise ValueError( | |||||
'pidfile {0.path} contents invalid.'.format(self)) | |||||
def remove(self): | |||||
"""Remove the lock.""" | |||||
with ignore_errno(errno.ENOENT, errno.EACCES): | |||||
os.unlink(self.path) | |||||
def remove_if_stale(self): | |||||
"""Remove the lock if the process is not running. | |||||
(does not respond to signals).""" | |||||
try: | |||||
pid = self.read_pid() | |||||
except ValueError as exc: | |||||
print('Broken pidfile found. Removing it.', file=sys.stderr) | |||||
self.remove() | |||||
return True | |||||
if not pid: | |||||
self.remove() | |||||
return True | |||||
try: | |||||
os.kill(pid, 0) | |||||
except os.error as exc: | |||||
if exc.errno == errno.ESRCH: | |||||
print('Stale pidfile exists. Removing it.', file=sys.stderr) | |||||
self.remove() | |||||
return True | |||||
return False | |||||
def write_pid(self): | |||||
pid = os.getpid() | |||||
content = '{0}\n'.format(pid) | |||||
pidfile_fd = os.open(self.path, PIDFILE_FLAGS, PIDFILE_MODE) | |||||
pidfile = os.fdopen(pidfile_fd, 'w') | |||||
try: | |||||
pidfile.write(content) | |||||
# flush and sync so that the re-read below works. | |||||
pidfile.flush() | |||||
try: | |||||
os.fsync(pidfile_fd) | |||||
except AttributeError: # pragma: no cover | |||||
pass | |||||
finally: | |||||
pidfile.close() | |||||
rfh = open(self.path) | |||||
try: | |||||
if rfh.read() != content: | |||||
raise LockFailed( | |||||
"Inconsistency: Pidfile content doesn't match at re-read") | |||||
finally: | |||||
rfh.close() | |||||
PIDFile = Pidfile # compat alias | |||||
def create_pidlock(pidfile): | |||||
"""Create and verify pidfile. | |||||
If the pidfile already exists the program exits with an error message, | |||||
however if the process it refers to is not running anymore, the pidfile | |||||
is deleted and the program continues. | |||||
This function will automatically install an :mod:`atexit` handler | |||||
to release the lock at exit, you can skip this by calling | |||||
:func:`_create_pidlock` instead. | |||||
:returns: :class:`Pidfile`. | |||||
**Example**: | |||||
.. code-block:: python | |||||
pidlock = create_pidlock('/var/run/app.pid') | |||||
""" | |||||
pidlock = _create_pidlock(pidfile) | |||||
atexit.register(pidlock.release) | |||||
return pidlock | |||||
def _create_pidlock(pidfile): | |||||
pidlock = Pidfile(pidfile) | |||||
if pidlock.is_locked() and not pidlock.remove_if_stale(): | |||||
print(PIDLOCKED.format(pidfile, pidlock.read_pid()), file=sys.stderr) | |||||
raise SystemExit(EX_CANTCREAT) | |||||
pidlock.acquire() | |||||
return pidlock | |||||
def fd_by_path(paths): | |||||
"""Return a list of fds. | |||||
This method returns list of fds corresponding to | |||||
file paths passed in paths variable. | |||||
:keyword paths: List of file paths go get fd for. | |||||
:returns: :list:. | |||||
**Example**: | |||||
.. code-block:: python | |||||
keep = fd_by_path(['/dev/urandom', | |||||
'/my/precious/']) | |||||
""" | |||||
stats = set() | |||||
for path in paths: | |||||
try: | |||||
fd = os.open(path, os.O_RDONLY) | |||||
except OSError: | |||||
continue | |||||
try: | |||||
stats.add(os.fstat(fd)[1:3]) | |||||
finally: | |||||
os.close(fd) | |||||
def fd_in_stats(fd): | |||||
try: | |||||
return os.fstat(fd)[1:3] in stats | |||||
except OSError: | |||||
return False | |||||
return [_fd for _fd in range(get_fdmax(2048)) if fd_in_stats(_fd)] | |||||
if hasattr(os, 'closerange'): | |||||
def close_open_fds(keep=None): | |||||
# must make sure this is 0-inclusive (Issue #1882) | |||||
keep = list(uniq(sorted( | |||||
f for f in map(maybe_fileno, keep or []) if f is not None | |||||
))) | |||||
maxfd = get_fdmax(default=2048) | |||||
kL, kH = iter([-1] + keep), iter(keep + [maxfd]) | |||||
for low, high in zip_longest(kL, kH): | |||||
if low + 1 != high: | |||||
os.closerange(low + 1, high) | |||||
else: | |||||
def close_open_fds(keep=None): # noqa | |||||
keep = [maybe_fileno(f) | |||||
for f in (keep or []) if maybe_fileno(f) is not None] | |||||
for fd in reversed(range(get_fdmax(default=2048))): | |||||
if fd not in keep: | |||||
with ignore_errno(errno.EBADF): | |||||
os.close(fd) | |||||
class DaemonContext(object): | |||||
_is_open = False | |||||
def __init__(self, pidfile=None, workdir=None, umask=None, | |||||
fake=False, after_chdir=None, after_forkers=True, | |||||
**kwargs): | |||||
if isinstance(umask, string_t): | |||||
# octal or decimal, depending on initial zero. | |||||
umask = int(umask, 8 if umask.startswith('0') else 10) | |||||
self.workdir = workdir or DAEMON_WORKDIR | |||||
self.umask = umask | |||||
self.fake = fake | |||||
self.after_chdir = after_chdir | |||||
self.after_forkers = after_forkers | |||||
self.stdfds = (sys.stdin, sys.stdout, sys.stderr) | |||||
def redirect_to_null(self, fd): | |||||
if fd is not None: | |||||
dest = os.open(os.devnull, os.O_RDWR) | |||||
os.dup2(dest, fd) | |||||
def open(self): | |||||
if not self._is_open: | |||||
if not self.fake: | |||||
self._detach() | |||||
os.chdir(self.workdir) | |||||
if self.umask is not None: | |||||
os.umask(self.umask) | |||||
if self.after_chdir: | |||||
self.after_chdir() | |||||
if not self.fake: | |||||
# We need to keep /dev/urandom from closing because | |||||
# shelve needs it, and Beat needs shelve to start. | |||||
keep = list(self.stdfds) + fd_by_path(['/dev/urandom']) | |||||
close_open_fds(keep) | |||||
for fd in self.stdfds: | |||||
self.redirect_to_null(maybe_fileno(fd)) | |||||
if self.after_forkers and mputil is not None: | |||||
mputil._run_after_forkers() | |||||
self._is_open = True | |||||
__enter__ = open | |||||
def close(self, *args): | |||||
if self._is_open: | |||||
self._is_open = False | |||||
__exit__ = close | |||||
def _detach(self): | |||||
if os.fork() == 0: # first child | |||||
os.setsid() # create new session | |||||
if os.fork() > 0: # second child | |||||
os._exit(0) | |||||
else: | |||||
os._exit(0) | |||||
return self | |||||
def detached(logfile=None, pidfile=None, uid=None, gid=None, umask=0, | |||||
workdir=None, fake=False, **opts): | |||||
"""Detach the current process in the background (daemonize). | |||||
:keyword logfile: Optional log file. The ability to write to this file | |||||
will be verified before the process is detached. | |||||
:keyword pidfile: Optional pidfile. The pidfile will not be created, | |||||
as this is the responsibility of the child. But the process will | |||||
exit if the pid lock exists and the pid written is still running. | |||||
:keyword uid: Optional user id or user name to change | |||||
effective privileges to. | |||||
:keyword gid: Optional group id or group name to change effective | |||||
privileges to. | |||||
:keyword umask: Optional umask that will be effective in the child process. | |||||
:keyword workdir: Optional new working directory. | |||||
:keyword fake: Don't actually detach, intented for debugging purposes. | |||||
:keyword \*\*opts: Ignored. | |||||
**Example**: | |||||
.. code-block:: python | |||||
from celery.platforms import detached, create_pidlock | |||||
with detached(logfile='/var/log/app.log', pidfile='/var/run/app.pid', | |||||
uid='nobody'): | |||||
# Now in detached child process with effective user set to nobody, | |||||
# and we know that our logfile can be written to, and that | |||||
# the pidfile is not locked. | |||||
pidlock = create_pidlock('/var/run/app.pid') | |||||
# Run the program | |||||
program.run(logfile='/var/log/app.log') | |||||
""" | |||||
if not resource: | |||||
raise RuntimeError('This platform does not support detach.') | |||||
workdir = os.getcwd() if workdir is None else workdir | |||||
signals.reset('SIGCLD') # Make sure SIGCLD is using the default handler. | |||||
maybe_drop_privileges(uid=uid, gid=gid) | |||||
def after_chdir_do(): | |||||
# Since without stderr any errors will be silently suppressed, | |||||
# we need to know that we have access to the logfile. | |||||
logfile and open(logfile, 'a').close() | |||||
# Doesn't actually create the pidfile, but makes sure it's not stale. | |||||
if pidfile: | |||||
_create_pidlock(pidfile).release() | |||||
return DaemonContext( | |||||
umask=umask, workdir=workdir, fake=fake, after_chdir=after_chdir_do, | |||||
) | |||||
def parse_uid(uid): | |||||
"""Parse user id. | |||||
uid can be an integer (uid) or a string (user name), if a user name | |||||
the uid is taken from the system user registry. | |||||
""" | |||||
try: | |||||
return int(uid) | |||||
except ValueError: | |||||
try: | |||||
return pwd.getpwnam(uid).pw_uid | |||||
except (AttributeError, KeyError): | |||||
raise KeyError('User does not exist: {0}'.format(uid)) | |||||
def parse_gid(gid): | |||||
"""Parse group id. | |||||
gid can be an integer (gid) or a string (group name), if a group name | |||||
the gid is taken from the system group registry. | |||||
""" | |||||
try: | |||||
return int(gid) | |||||
except ValueError: | |||||
try: | |||||
return grp.getgrnam(gid).gr_gid | |||||
except (AttributeError, KeyError): | |||||
raise KeyError('Group does not exist: {0}'.format(gid)) | |||||
def _setgroups_hack(groups): | |||||
""":fun:`setgroups` may have a platform-dependent limit, | |||||
and it is not always possible to know in advance what this limit | |||||
is, so we use this ugly hack stolen from glibc.""" | |||||
groups = groups[:] | |||||
while 1: | |||||
try: | |||||
return os.setgroups(groups) | |||||
except ValueError: # error from Python's check. | |||||
if len(groups) <= 1: | |||||
raise | |||||
groups[:] = groups[:-1] | |||||
except OSError as exc: # error from the OS. | |||||
if exc.errno != errno.EINVAL or len(groups) <= 1: | |||||
raise | |||||
groups[:] = groups[:-1] | |||||
def setgroups(groups): | |||||
"""Set active groups from a list of group ids.""" | |||||
max_groups = None | |||||
try: | |||||
max_groups = os.sysconf('SC_NGROUPS_MAX') | |||||
except Exception: | |||||
pass | |||||
try: | |||||
return _setgroups_hack(groups[:max_groups]) | |||||
except OSError as exc: | |||||
if exc.errno != errno.EPERM: | |||||
raise | |||||
if any(group not in groups for group in os.getgroups()): | |||||
# we shouldn't be allowed to change to this group. | |||||
raise | |||||
def initgroups(uid, gid): | |||||
"""Compat version of :func:`os.initgroups` which was first | |||||
added to Python 2.7.""" | |||||
if not pwd: # pragma: no cover | |||||
return | |||||
username = pwd.getpwuid(uid)[0] | |||||
if hasattr(os, 'initgroups'): # Python 2.7+ | |||||
return os.initgroups(username, gid) | |||||
groups = [gr.gr_gid for gr in grp.getgrall() | |||||
if username in gr.gr_mem] | |||||
setgroups(groups) | |||||
def setgid(gid): | |||||
"""Version of :func:`os.setgid` supporting group names.""" | |||||
os.setgid(parse_gid(gid)) | |||||
def setuid(uid): | |||||
"""Version of :func:`os.setuid` supporting usernames.""" | |||||
os.setuid(parse_uid(uid)) | |||||
def maybe_drop_privileges(uid=None, gid=None): | |||||
"""Change process privileges to new user/group. | |||||
If UID and GID is specified, the real user/group is changed. | |||||
If only UID is specified, the real user is changed, and the group is | |||||
changed to the users primary group. | |||||
If only GID is specified, only the group is changed. | |||||
""" | |||||
if sys.platform == 'win32': | |||||
return | |||||
if os.geteuid(): | |||||
# no point trying to setuid unless we're root. | |||||
if not os.getuid(): | |||||
raise AssertionError('contact support') | |||||
uid = uid and parse_uid(uid) | |||||
gid = gid and parse_gid(gid) | |||||
if uid: | |||||
# If GID isn't defined, get the primary GID of the user. | |||||
if not gid and pwd: | |||||
gid = pwd.getpwuid(uid).pw_gid | |||||
# Must set the GID before initgroups(), as setgid() | |||||
# is known to zap the group list on some platforms. | |||||
# setgid must happen before setuid (otherwise the setgid operation | |||||
# may fail because of insufficient privileges and possibly stay | |||||
# in a privileged group). | |||||
setgid(gid) | |||||
initgroups(uid, gid) | |||||
# at last: | |||||
setuid(uid) | |||||
# ... and make sure privileges cannot be restored: | |||||
try: | |||||
setuid(0) | |||||
except OSError as exc: | |||||
if get_errno(exc) != errno.EPERM: | |||||
raise | |||||
pass # Good: cannot restore privileges. | |||||
else: | |||||
raise RuntimeError( | |||||
'non-root user able to restore privileges after setuid.') | |||||
else: | |||||
gid and setgid(gid) | |||||
if uid and (not os.getuid()) and not (os.geteuid()): | |||||
raise AssertionError('Still root uid after drop privileges!') | |||||
if gid and (not os.getgid()) and not (os.getegid()): | |||||
raise AssertionError('Still root gid after drop privileges!') | |||||
class Signals(object): | |||||
"""Convenience interface to :mod:`signals`. | |||||
If the requested signal is not supported on the current platform, | |||||
the operation will be ignored. | |||||
**Examples**: | |||||
.. code-block:: python | |||||
>>> from celery.platforms import signals | |||||
>>> from proj.handlers import my_handler | |||||
>>> signals['INT'] = my_handler | |||||
>>> signals['INT'] | |||||
my_handler | |||||
>>> signals.supported('INT') | |||||
True | |||||
>>> signals.signum('INT') | |||||
2 | |||||
>>> signals.ignore('USR1') | |||||
>>> signals['USR1'] == signals.ignored | |||||
True | |||||
>>> signals.reset('USR1') | |||||
>>> signals['USR1'] == signals.default | |||||
True | |||||
>>> from proj.handlers import exit_handler, hup_handler | |||||
>>> signals.update(INT=exit_handler, | |||||
... TERM=exit_handler, | |||||
... HUP=hup_handler) | |||||
""" | |||||
ignored = _signal.SIG_IGN | |||||
default = _signal.SIG_DFL | |||||
if hasattr(_signal, 'setitimer'): | |||||
def arm_alarm(self, seconds): | |||||
_signal.setitimer(_signal.ITIMER_REAL, seconds) | |||||
else: # pragma: no cover | |||||
try: | |||||
from itimer import alarm as _itimer_alarm # noqa | |||||
except ImportError: | |||||
def arm_alarm(self, seconds): # noqa | |||||
_signal.alarm(math.ceil(seconds)) | |||||
else: # pragma: no cover | |||||
def arm_alarm(self, seconds): # noqa | |||||
return _itimer_alarm(seconds) # noqa | |||||
def reset_alarm(self): | |||||
return _signal.alarm(0) | |||||
def supported(self, signal_name): | |||||
"""Return true value if ``signal_name`` exists on this platform.""" | |||||
try: | |||||
return self.signum(signal_name) | |||||
except AttributeError: | |||||
pass | |||||
def signum(self, signal_name): | |||||
"""Get signal number from signal name.""" | |||||
if isinstance(signal_name, numbers.Integral): | |||||
return signal_name | |||||
if not isinstance(signal_name, string_t) \ | |||||
or not signal_name.isupper(): | |||||
raise TypeError('signal name must be uppercase string.') | |||||
if not signal_name.startswith('SIG'): | |||||
signal_name = 'SIG' + signal_name | |||||
return getattr(_signal, signal_name) | |||||
def reset(self, *signal_names): | |||||
"""Reset signals to the default signal handler. | |||||
Does nothing if the platform doesn't support signals, | |||||
or the specified signal in particular. | |||||
""" | |||||
self.update((sig, self.default) for sig in signal_names) | |||||
def ignore(self, *signal_names): | |||||
"""Ignore signal using :const:`SIG_IGN`. | |||||
Does nothing if the platform doesn't support signals, | |||||
or the specified signal in particular. | |||||
""" | |||||
self.update((sig, self.ignored) for sig in signal_names) | |||||
def __getitem__(self, signal_name): | |||||
return _signal.getsignal(self.signum(signal_name)) | |||||
def __setitem__(self, signal_name, handler): | |||||
"""Install signal handler. | |||||
Does nothing if the current platform doesn't support signals, | |||||
or the specified signal in particular. | |||||
""" | |||||
try: | |||||
_signal.signal(self.signum(signal_name), handler) | |||||
except (AttributeError, ValueError): | |||||
pass | |||||
def update(self, _d_=None, **sigmap): | |||||
"""Set signal handlers from a mapping.""" | |||||
for signal_name, handler in items(dict(_d_ or {}, **sigmap)): | |||||
self[signal_name] = handler | |||||
signals = Signals() | |||||
get_signal = signals.signum # compat | |||||
install_signal_handler = signals.__setitem__ # compat | |||||
reset_signal = signals.reset # compat | |||||
ignore_signal = signals.ignore # compat | |||||
def strargv(argv): | |||||
arg_start = 2 if 'manage' in argv[0] else 1 | |||||
if len(argv) > arg_start: | |||||
return ' '.join(argv[arg_start:]) | |||||
return '' | |||||
def set_process_title(progname, info=None): | |||||
"""Set the ps name for the currently running process. | |||||
Only works if :mod:`setproctitle` is installed. | |||||
""" | |||||
proctitle = '[{0}]'.format(progname) | |||||
proctitle = '{0} {1}'.format(proctitle, info) if info else proctitle | |||||
if _setproctitle: | |||||
_setproctitle.setproctitle(safe_str(proctitle)) | |||||
return proctitle | |||||
if os.environ.get('NOSETPS'): # pragma: no cover | |||||
def set_mp_process_title(*a, **k): | |||||
pass | |||||
else: | |||||
def set_mp_process_title(progname, info=None, hostname=None): # noqa | |||||
"""Set the ps name using the multiprocessing process name. | |||||
Only works if :mod:`setproctitle` is installed. | |||||
""" | |||||
if hostname: | |||||
progname = '{0}: {1}'.format(progname, hostname) | |||||
return set_process_title( | |||||
'{0}:{1}'.format(progname, current_process().name), info=info) | |||||
def get_errno_name(n): | |||||
"""Get errno for string, e.g. ``ENOENT``.""" | |||||
if isinstance(n, string_t): | |||||
return getattr(errno, n) | |||||
return n | |||||
@contextmanager | |||||
def ignore_errno(*errnos, **kwargs): | |||||
"""Context manager to ignore specific POSIX error codes. | |||||
Takes a list of error codes to ignore, which can be either | |||||
the name of the code, or the code integer itself:: | |||||
>>> with ignore_errno('ENOENT'): | |||||
... with open('foo', 'r') as fh: | |||||
... return fh.read() | |||||
>>> with ignore_errno(errno.ENOENT, errno.EPERM): | |||||
... pass | |||||
:keyword types: A tuple of exceptions to ignore (when the errno matches), | |||||
defaults to :exc:`Exception`. | |||||
""" | |||||
types = kwargs.get('types') or (Exception, ) | |||||
errnos = [get_errno_name(errno) for errno in errnos] | |||||
try: | |||||
yield | |||||
except types as exc: | |||||
if not hasattr(exc, 'errno'): | |||||
raise | |||||
if exc.errno not in errnos: | |||||
raise | |||||
def check_privileges(accept_content): | |||||
uid = os.getuid() if hasattr(os, 'getuid') else 65535 | |||||
gid = os.getgid() if hasattr(os, 'getgid') else 65535 | |||||
euid = os.geteuid() if hasattr(os, 'geteuid') else 65535 | |||||
egid = os.getegid() if hasattr(os, 'getegid') else 65535 | |||||
if hasattr(os, 'fchown'): | |||||
if not all(hasattr(os, attr) | |||||
for attr in ['getuid', 'getgid', 'geteuid', 'getegid']): | |||||
raise AssertionError('suspicious platform, contact support') | |||||
if not uid or not gid or not euid or not egid: | |||||
if ('pickle' in accept_content or | |||||
'application/x-python-serialize' in accept_content): | |||||
if not C_FORCE_ROOT: | |||||
try: | |||||
print(ROOT_DISALLOWED.format( | |||||
uid=uid, euid=euid, gid=gid, egid=egid, | |||||
), file=sys.stderr) | |||||
finally: | |||||
os._exit(1) | |||||
warnings.warn(RuntimeWarning(ROOT_DISCOURAGED.format( | |||||
uid=uid, euid=euid, gid=gid, egid=egid, | |||||
))) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.result | |||||
~~~~~~~~~~~~~ | |||||
Task results/state and groups of results. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import time | |||||
import warnings | |||||
from collections import deque | |||||
from contextlib import contextmanager | |||||
from copy import copy | |||||
from kombu.utils import cached_property | |||||
from kombu.utils.compat import OrderedDict | |||||
from . import current_app | |||||
from . import states | |||||
from ._state import _set_task_join_will_block, task_join_will_block | |||||
from .app import app_or_default | |||||
from .datastructures import DependencyGraph, GraphFormatter | |||||
from .exceptions import IncompleteStream, TimeoutError | |||||
from .five import items, range, string_t, monotonic | |||||
from .utils import deprecated | |||||
__all__ = ['ResultBase', 'AsyncResult', 'ResultSet', 'GroupResult', | |||||
'EagerResult', 'result_from_tuple'] | |||||
E_WOULDBLOCK = """\ | |||||
Never call result.get() within a task! | |||||
See http://docs.celeryq.org/en/latest/userguide/tasks.html\ | |||||
#task-synchronous-subtasks | |||||
In Celery 3.2 this will result in an exception being | |||||
raised instead of just being a warning. | |||||
""" | |||||
def assert_will_not_block(): | |||||
if task_join_will_block(): | |||||
warnings.warn(RuntimeWarning(E_WOULDBLOCK)) | |||||
@contextmanager | |||||
def allow_join_result(): | |||||
reset_value = task_join_will_block() | |||||
_set_task_join_will_block(False) | |||||
try: | |||||
yield | |||||
finally: | |||||
_set_task_join_will_block(reset_value) | |||||
class ResultBase(object): | |||||
"""Base class for all results""" | |||||
#: Parent result (if part of a chain) | |||||
parent = None | |||||
class AsyncResult(ResultBase): | |||||
"""Query task state. | |||||
:param id: see :attr:`id`. | |||||
:keyword backend: see :attr:`backend`. | |||||
""" | |||||
app = None | |||||
#: Error raised for timeouts. | |||||
TimeoutError = TimeoutError | |||||
#: The task's UUID. | |||||
id = None | |||||
#: The task result backend to use. | |||||
backend = None | |||||
def __init__(self, id, backend=None, task_name=None, | |||||
app=None, parent=None): | |||||
self.app = app_or_default(app or self.app) | |||||
self.id = id | |||||
self.backend = backend or self.app.backend | |||||
self.task_name = task_name | |||||
self.parent = parent | |||||
self._cache = None | |||||
def as_tuple(self): | |||||
parent = self.parent | |||||
return (self.id, parent and parent.as_tuple()), None | |||||
serializable = as_tuple # XXX compat | |||||
def forget(self): | |||||
"""Forget about (and possibly remove the result of) this task.""" | |||||
self._cache = None | |||||
self.backend.forget(self.id) | |||||
def revoke(self, connection=None, terminate=False, signal=None, | |||||
wait=False, timeout=None): | |||||
"""Send revoke signal to all workers. | |||||
Any worker receiving the task, or having reserved the | |||||
task, *must* ignore it. | |||||
:keyword terminate: Also terminate the process currently working | |||||
on the task (if any). | |||||
:keyword signal: Name of signal to send to process if terminate. | |||||
Default is TERM. | |||||
:keyword wait: Wait for replies from workers. Will wait for 1 second | |||||
by default or you can specify a custom ``timeout``. | |||||
:keyword timeout: Time in seconds to wait for replies if ``wait`` | |||||
enabled. | |||||
""" | |||||
self.app.control.revoke(self.id, connection=connection, | |||||
terminate=terminate, signal=signal, | |||||
reply=wait, timeout=timeout) | |||||
def get(self, timeout=None, propagate=True, interval=0.5, | |||||
no_ack=True, follow_parents=True, | |||||
EXCEPTION_STATES=states.EXCEPTION_STATES, | |||||
PROPAGATE_STATES=states.PROPAGATE_STATES): | |||||
"""Wait until task is ready, and return its result. | |||||
.. warning:: | |||||
Waiting for tasks within a task may lead to deadlocks. | |||||
Please read :ref:`task-synchronous-subtasks`. | |||||
:keyword timeout: How long to wait, in seconds, before the | |||||
operation times out. | |||||
:keyword propagate: Re-raise exception if the task failed. | |||||
:keyword interval: Time to wait (in seconds) before retrying to | |||||
retrieve the result. Note that this does not have any effect | |||||
when using the amqp result store backend, as it does not | |||||
use polling. | |||||
:keyword no_ack: Enable amqp no ack (automatically acknowledge | |||||
message). If this is :const:`False` then the message will | |||||
**not be acked**. | |||||
:keyword follow_parents: Reraise any exception raised by parent task. | |||||
:raises celery.exceptions.TimeoutError: if `timeout` is not | |||||
:const:`None` and the result does not arrive within `timeout` | |||||
seconds. | |||||
If the remote call raised an exception then that exception will | |||||
be re-raised. | |||||
""" | |||||
assert_will_not_block() | |||||
on_interval = None | |||||
if follow_parents and propagate and self.parent: | |||||
on_interval = self._maybe_reraise_parent_error | |||||
on_interval() | |||||
if self._cache: | |||||
if propagate: | |||||
self.maybe_reraise() | |||||
return self.result | |||||
meta = self.backend.wait_for( | |||||
self.id, timeout=timeout, | |||||
interval=interval, | |||||
on_interval=on_interval, | |||||
no_ack=no_ack, | |||||
) | |||||
if meta: | |||||
self._maybe_set_cache(meta) | |||||
status = meta['status'] | |||||
if status in PROPAGATE_STATES and propagate: | |||||
raise meta['result'] | |||||
return meta['result'] | |||||
wait = get # deprecated alias to :meth:`get`. | |||||
def _maybe_reraise_parent_error(self): | |||||
for node in reversed(list(self._parents())): | |||||
node.maybe_reraise() | |||||
def _parents(self): | |||||
node = self.parent | |||||
while node: | |||||
yield node | |||||
node = node.parent | |||||
def collect(self, intermediate=False, **kwargs): | |||||
"""Iterator, like :meth:`get` will wait for the task to complete, | |||||
but will also follow :class:`AsyncResult` and :class:`ResultSet` | |||||
returned by the task, yielding ``(result, value)`` tuples for each | |||||
result in the tree. | |||||
An example would be having the following tasks: | |||||
.. code-block:: python | |||||
from celery import group | |||||
from proj.celery import app | |||||
@app.task(trail=True) | |||||
def A(how_many): | |||||
return group(B.s(i) for i in range(how_many))() | |||||
@app.task(trail=True) | |||||
def B(i): | |||||
return pow2.delay(i) | |||||
@app.task(trail=True) | |||||
def pow2(i): | |||||
return i ** 2 | |||||
Note that the ``trail`` option must be enabled | |||||
so that the list of children is stored in ``result.children``. | |||||
This is the default but enabled explicitly for illustration. | |||||
Calling :meth:`collect` would return: | |||||
.. code-block:: python | |||||
>>> from celery.result import ResultBase | |||||
>>> from proj.tasks import A | |||||
>>> result = A.delay(10) | |||||
>>> [v for v in result.collect() | |||||
... if not isinstance(v, (ResultBase, tuple))] | |||||
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] | |||||
""" | |||||
for _, R in self.iterdeps(intermediate=intermediate): | |||||
yield R, R.get(**kwargs) | |||||
def get_leaf(self): | |||||
value = None | |||||
for _, R in self.iterdeps(): | |||||
value = R.get() | |||||
return value | |||||
def iterdeps(self, intermediate=False): | |||||
stack = deque([(None, self)]) | |||||
while stack: | |||||
parent, node = stack.popleft() | |||||
yield parent, node | |||||
if node.ready(): | |||||
stack.extend((node, child) for child in node.children or []) | |||||
else: | |||||
if not intermediate: | |||||
raise IncompleteStream() | |||||
def ready(self): | |||||
"""Returns :const:`True` if the task has been executed. | |||||
If the task is still running, pending, or is waiting | |||||
for retry then :const:`False` is returned. | |||||
""" | |||||
return self.state in self.backend.READY_STATES | |||||
def successful(self): | |||||
"""Returns :const:`True` if the task executed successfully.""" | |||||
return self.state == states.SUCCESS | |||||
def failed(self): | |||||
"""Returns :const:`True` if the task failed.""" | |||||
return self.state == states.FAILURE | |||||
def maybe_reraise(self): | |||||
if self.state in states.PROPAGATE_STATES: | |||||
raise self.result | |||||
def build_graph(self, intermediate=False, formatter=None): | |||||
graph = DependencyGraph( | |||||
formatter=formatter or GraphFormatter(root=self.id, shape='oval'), | |||||
) | |||||
for parent, node in self.iterdeps(intermediate=intermediate): | |||||
graph.add_arc(node) | |||||
if parent: | |||||
graph.add_edge(parent, node) | |||||
return graph | |||||
def __str__(self): | |||||
"""`str(self) -> self.id`""" | |||||
return str(self.id) | |||||
def __hash__(self): | |||||
"""`hash(self) -> hash(self.id)`""" | |||||
return hash(self.id) | |||||
def __repr__(self): | |||||
return '<{0}: {1}>'.format(type(self).__name__, self.id) | |||||
def __eq__(self, other): | |||||
if isinstance(other, AsyncResult): | |||||
return other.id == self.id | |||||
elif isinstance(other, string_t): | |||||
return other == self.id | |||||
return NotImplemented | |||||
def __ne__(self, other): | |||||
return not self.__eq__(other) | |||||
def __copy__(self): | |||||
return self.__class__( | |||||
self.id, self.backend, self.task_name, self.app, self.parent, | |||||
) | |||||
def __reduce__(self): | |||||
return self.__class__, self.__reduce_args__() | |||||
def __reduce_args__(self): | |||||
return self.id, self.backend, self.task_name, None, self.parent | |||||
def __del__(self): | |||||
self._cache = None | |||||
@cached_property | |||||
def graph(self): | |||||
return self.build_graph() | |||||
@property | |||||
def supports_native_join(self): | |||||
return self.backend.supports_native_join | |||||
@property | |||||
def children(self): | |||||
return self._get_task_meta().get('children') | |||||
def _maybe_set_cache(self, meta): | |||||
if meta: | |||||
state = meta['status'] | |||||
if state == states.SUCCESS or state in states.PROPAGATE_STATES: | |||||
return self._set_cache(meta) | |||||
return meta | |||||
def _get_task_meta(self): | |||||
if self._cache is None: | |||||
return self._maybe_set_cache(self.backend.get_task_meta(self.id)) | |||||
return self._cache | |||||
def _set_cache(self, d): | |||||
children = d.get('children') | |||||
if children: | |||||
d['children'] = [ | |||||
result_from_tuple(child, self.app) for child in children | |||||
] | |||||
self._cache = d | |||||
return d | |||||
@property | |||||
def result(self): | |||||
"""When the task has been executed, this contains the return value. | |||||
If the task raised an exception, this will be the exception | |||||
instance.""" | |||||
return self._get_task_meta()['result'] | |||||
info = result | |||||
@property | |||||
def traceback(self): | |||||
"""Get the traceback of a failed task.""" | |||||
return self._get_task_meta().get('traceback') | |||||
@property | |||||
def state(self): | |||||
"""The tasks current state. | |||||
Possible values includes: | |||||
*PENDING* | |||||
The task is waiting for execution. | |||||
*STARTED* | |||||
The task has been started. | |||||
*RETRY* | |||||
The task is to be retried, possibly because of failure. | |||||
*FAILURE* | |||||
The task raised an exception, or has exceeded the retry limit. | |||||
The :attr:`result` attribute then contains the | |||||
exception raised by the task. | |||||
*SUCCESS* | |||||
The task executed successfully. The :attr:`result` attribute | |||||
then contains the tasks return value. | |||||
""" | |||||
return self._get_task_meta()['status'] | |||||
status = state | |||||
@property | |||||
def task_id(self): | |||||
"""compat alias to :attr:`id`""" | |||||
return self.id | |||||
@task_id.setter # noqa | |||||
def task_id(self, id): | |||||
self.id = id | |||||
BaseAsyncResult = AsyncResult # for backwards compatibility. | |||||
class ResultSet(ResultBase): | |||||
"""Working with more than one result. | |||||
:param results: List of result instances. | |||||
""" | |||||
app = None | |||||
#: List of results in in the set. | |||||
results = None | |||||
def __init__(self, results, app=None, **kwargs): | |||||
self.app = app_or_default(app or self.app) | |||||
self.results = results | |||||
def add(self, result): | |||||
"""Add :class:`AsyncResult` as a new member of the set. | |||||
Does nothing if the result is already a member. | |||||
""" | |||||
if result not in self.results: | |||||
self.results.append(result) | |||||
def remove(self, result): | |||||
"""Remove result from the set; it must be a member. | |||||
:raises KeyError: if the result is not a member. | |||||
""" | |||||
if isinstance(result, string_t): | |||||
result = self.app.AsyncResult(result) | |||||
try: | |||||
self.results.remove(result) | |||||
except ValueError: | |||||
raise KeyError(result) | |||||
def discard(self, result): | |||||
"""Remove result from the set if it is a member. | |||||
If it is not a member, do nothing. | |||||
""" | |||||
try: | |||||
self.remove(result) | |||||
except KeyError: | |||||
pass | |||||
def update(self, results): | |||||
"""Update set with the union of itself and an iterable with | |||||
results.""" | |||||
self.results.extend(r for r in results if r not in self.results) | |||||
def clear(self): | |||||
"""Remove all results from this set.""" | |||||
self.results[:] = [] # don't create new list. | |||||
def successful(self): | |||||
"""Was all of the tasks successful? | |||||
:returns: :const:`True` if all of the tasks finished | |||||
successfully (i.e. did not raise an exception). | |||||
""" | |||||
return all(result.successful() for result in self.results) | |||||
def failed(self): | |||||
"""Did any of the tasks fail? | |||||
:returns: :const:`True` if one of the tasks failed. | |||||
(i.e., raised an exception) | |||||
""" | |||||
return any(result.failed() for result in self.results) | |||||
def maybe_reraise(self): | |||||
for result in self.results: | |||||
result.maybe_reraise() | |||||
def waiting(self): | |||||
"""Are any of the tasks incomplete? | |||||
:returns: :const:`True` if one of the tasks are still | |||||
waiting for execution. | |||||
""" | |||||
return any(not result.ready() for result in self.results) | |||||
def ready(self): | |||||
"""Did all of the tasks complete? (either by success of failure). | |||||
:returns: :const:`True` if all of the tasks has been | |||||
executed. | |||||
""" | |||||
return all(result.ready() for result in self.results) | |||||
def completed_count(self): | |||||
"""Task completion count. | |||||
:returns: the number of tasks completed. | |||||
""" | |||||
return sum(int(result.successful()) for result in self.results) | |||||
def forget(self): | |||||
"""Forget about (and possible remove the result of) all the tasks.""" | |||||
for result in self.results: | |||||
result.forget() | |||||
def revoke(self, connection=None, terminate=False, signal=None, | |||||
wait=False, timeout=None): | |||||
"""Send revoke signal to all workers for all tasks in the set. | |||||
:keyword terminate: Also terminate the process currently working | |||||
on the task (if any). | |||||
:keyword signal: Name of signal to send to process if terminate. | |||||
Default is TERM. | |||||
:keyword wait: Wait for replies from worker. Will wait for 1 second | |||||
by default or you can specify a custom ``timeout``. | |||||
:keyword timeout: Time in seconds to wait for replies if ``wait`` | |||||
enabled. | |||||
""" | |||||
self.app.control.revoke([r.id for r in self.results], | |||||
connection=connection, timeout=timeout, | |||||
terminate=terminate, signal=signal, reply=wait) | |||||
def __iter__(self): | |||||
return iter(self.results) | |||||
def __getitem__(self, index): | |||||
"""`res[i] -> res.results[i]`""" | |||||
return self.results[index] | |||||
@deprecated('3.2', '3.3') | |||||
def iterate(self, timeout=None, propagate=True, interval=0.5): | |||||
"""Deprecated method, use :meth:`get` with a callback argument.""" | |||||
elapsed = 0.0 | |||||
results = OrderedDict((result.id, copy(result)) | |||||
for result in self.results) | |||||
while results: | |||||
removed = set() | |||||
for task_id, result in items(results): | |||||
if result.ready(): | |||||
yield result.get(timeout=timeout and timeout - elapsed, | |||||
propagate=propagate) | |||||
removed.add(task_id) | |||||
else: | |||||
if result.backend.subpolling_interval: | |||||
time.sleep(result.backend.subpolling_interval) | |||||
for task_id in removed: | |||||
results.pop(task_id, None) | |||||
time.sleep(interval) | |||||
elapsed += interval | |||||
if timeout and elapsed >= timeout: | |||||
raise TimeoutError('The operation timed out') | |||||
def get(self, timeout=None, propagate=True, interval=0.5, | |||||
callback=None, no_ack=True): | |||||
"""See :meth:`join` | |||||
This is here for API compatibility with :class:`AsyncResult`, | |||||
in addition it uses :meth:`join_native` if available for the | |||||
current result backend. | |||||
""" | |||||
return (self.join_native if self.supports_native_join else self.join)( | |||||
timeout=timeout, propagate=propagate, | |||||
interval=interval, callback=callback, no_ack=no_ack) | |||||
def join(self, timeout=None, propagate=True, interval=0.5, | |||||
callback=None, no_ack=True): | |||||
"""Gathers the results of all tasks as a list in order. | |||||
.. note:: | |||||
This can be an expensive operation for result store | |||||
backends that must resort to polling (e.g. database). | |||||
You should consider using :meth:`join_native` if your backend | |||||
supports it. | |||||
.. warning:: | |||||
Waiting for tasks within a task may lead to deadlocks. | |||||
Please see :ref:`task-synchronous-subtasks`. | |||||
:keyword timeout: The number of seconds to wait for results before | |||||
the operation times out. | |||||
:keyword propagate: If any of the tasks raises an exception, the | |||||
exception will be re-raised. | |||||
:keyword interval: Time to wait (in seconds) before retrying to | |||||
retrieve a result from the set. Note that this | |||||
does not have any effect when using the amqp | |||||
result store backend, as it does not use polling. | |||||
:keyword callback: Optional callback to be called for every result | |||||
received. Must have signature ``(task_id, value)`` | |||||
No results will be returned by this function if | |||||
a callback is specified. The order of results | |||||
is also arbitrary when a callback is used. | |||||
To get access to the result object for a particular | |||||
id you will have to generate an index first: | |||||
``index = {r.id: r for r in gres.results.values()}`` | |||||
Or you can create new result objects on the fly: | |||||
``result = app.AsyncResult(task_id)`` (both will | |||||
take advantage of the backend cache anyway). | |||||
:keyword no_ack: Automatic message acknowledgement (Note that if this | |||||
is set to :const:`False` then the messages *will not be | |||||
acknowledged*). | |||||
:raises celery.exceptions.TimeoutError: if ``timeout`` is not | |||||
:const:`None` and the operation takes longer than ``timeout`` | |||||
seconds. | |||||
""" | |||||
assert_will_not_block() | |||||
time_start = monotonic() | |||||
remaining = None | |||||
results = [] | |||||
for result in self.results: | |||||
remaining = None | |||||
if timeout: | |||||
remaining = timeout - (monotonic() - time_start) | |||||
if remaining <= 0.0: | |||||
raise TimeoutError('join operation timed out') | |||||
value = result.get( | |||||
timeout=remaining, propagate=propagate, | |||||
interval=interval, no_ack=no_ack, | |||||
) | |||||
if callback: | |||||
callback(result.id, value) | |||||
else: | |||||
results.append(value) | |||||
return results | |||||
def iter_native(self, timeout=None, interval=0.5, no_ack=True): | |||||
"""Backend optimized version of :meth:`iterate`. | |||||
.. versionadded:: 2.2 | |||||
Note that this does not support collecting the results | |||||
for different task types using different backends. | |||||
This is currently only supported by the amqp, Redis and cache | |||||
result backends. | |||||
""" | |||||
results = self.results | |||||
if not results: | |||||
return iter([]) | |||||
return self.backend.get_many( | |||||
set(r.id for r in results), | |||||
timeout=timeout, interval=interval, no_ack=no_ack, | |||||
) | |||||
def join_native(self, timeout=None, propagate=True, | |||||
interval=0.5, callback=None, no_ack=True): | |||||
"""Backend optimized version of :meth:`join`. | |||||
.. versionadded:: 2.2 | |||||
Note that this does not support collecting the results | |||||
for different task types using different backends. | |||||
This is currently only supported by the amqp, Redis and cache | |||||
result backends. | |||||
""" | |||||
assert_will_not_block() | |||||
order_index = None if callback else dict( | |||||
(result.id, i) for i, result in enumerate(self.results) | |||||
) | |||||
acc = None if callback else [None for _ in range(len(self))] | |||||
for task_id, meta in self.iter_native(timeout, interval, no_ack): | |||||
value = meta['result'] | |||||
if propagate and meta['status'] in states.PROPAGATE_STATES: | |||||
raise value | |||||
if callback: | |||||
callback(task_id, value) | |||||
else: | |||||
acc[order_index[task_id]] = value | |||||
return acc | |||||
def _failed_join_report(self): | |||||
return (res for res in self.results | |||||
if res.backend.is_cached(res.id) and | |||||
res.state in states.PROPAGATE_STATES) | |||||
def __len__(self): | |||||
return len(self.results) | |||||
def __eq__(self, other): | |||||
if isinstance(other, ResultSet): | |||||
return other.results == self.results | |||||
return NotImplemented | |||||
def __ne__(self, other): | |||||
return not self.__eq__(other) | |||||
def __repr__(self): | |||||
return '<{0}: [{1}]>'.format(type(self).__name__, | |||||
', '.join(r.id for r in self.results)) | |||||
@property | |||||
def subtasks(self): | |||||
"""Deprecated alias to :attr:`results`.""" | |||||
return self.results | |||||
@property | |||||
def supports_native_join(self): | |||||
try: | |||||
return self.results[0].supports_native_join | |||||
except IndexError: | |||||
pass | |||||
@property | |||||
def backend(self): | |||||
return self.app.backend if self.app else self.results[0].backend | |||||
class GroupResult(ResultSet): | |||||
"""Like :class:`ResultSet`, but with an associated id. | |||||
This type is returned by :class:`~celery.group`, and the | |||||
deprecated TaskSet, meth:`~celery.task.TaskSet.apply_async` method. | |||||
It enables inspection of the tasks state and return values as | |||||
a single entity. | |||||
:param id: The id of the group. | |||||
:param results: List of result instances. | |||||
""" | |||||
#: The UUID of the group. | |||||
id = None | |||||
#: List/iterator of results in the group | |||||
results = None | |||||
def __init__(self, id=None, results=None, **kwargs): | |||||
self.id = id | |||||
ResultSet.__init__(self, results, **kwargs) | |||||
def save(self, backend=None): | |||||
"""Save group-result for later retrieval using :meth:`restore`. | |||||
Example:: | |||||
>>> def save_and_restore(result): | |||||
... result.save() | |||||
... result = GroupResult.restore(result.id) | |||||
""" | |||||
return (backend or self.app.backend).save_group(self.id, self) | |||||
def delete(self, backend=None): | |||||
"""Remove this result if it was previously saved.""" | |||||
(backend or self.app.backend).delete_group(self.id) | |||||
def __reduce__(self): | |||||
return self.__class__, self.__reduce_args__() | |||||
def __reduce_args__(self): | |||||
return self.id, self.results | |||||
def __bool__(self): | |||||
return bool(self.id or self.results) | |||||
__nonzero__ = __bool__ # Included for Py2 backwards compatibility | |||||
def __eq__(self, other): | |||||
if isinstance(other, GroupResult): | |||||
return other.id == self.id and other.results == self.results | |||||
return NotImplemented | |||||
def __ne__(self, other): | |||||
return not self.__eq__(other) | |||||
def __repr__(self): | |||||
return '<{0}: {1} [{2}]>'.format(type(self).__name__, self.id, | |||||
', '.join(r.id for r in self.results)) | |||||
def as_tuple(self): | |||||
return self.id, [r.as_tuple() for r in self.results] | |||||
serializable = as_tuple # XXX compat | |||||
@property | |||||
def children(self): | |||||
return self.results | |||||
@classmethod | |||||
def restore(self, id, backend=None): | |||||
"""Restore previously saved group result.""" | |||||
return ( | |||||
backend or (self.app.backend if self.app else current_app.backend) | |||||
).restore_group(id) | |||||
class TaskSetResult(GroupResult): | |||||
"""Deprecated version of :class:`GroupResult`""" | |||||
def __init__(self, taskset_id, results=None, **kwargs): | |||||
# XXX supports the taskset_id kwarg. | |||||
# XXX previously the "results" arg was named "subtasks". | |||||
if 'subtasks' in kwargs: | |||||
results = kwargs['subtasks'] | |||||
GroupResult.__init__(self, taskset_id, results, **kwargs) | |||||
def itersubtasks(self): | |||||
"""Deprecated. Use ``iter(self.results)`` instead.""" | |||||
return iter(self.results) | |||||
@property | |||||
def total(self): | |||||
"""Deprecated: Use ``len(r)``.""" | |||||
return len(self) | |||||
@property | |||||
def taskset_id(self): | |||||
"""compat alias to :attr:`self.id`""" | |||||
return self.id | |||||
@taskset_id.setter # noqa | |||||
def taskset_id(self, id): | |||||
self.id = id | |||||
class EagerResult(AsyncResult): | |||||
"""Result that we know has already been executed.""" | |||||
task_name = None | |||||
def __init__(self, id, ret_value, state, traceback=None): | |||||
self.id = id | |||||
self._result = ret_value | |||||
self._state = state | |||||
self._traceback = traceback | |||||
def _get_task_meta(self): | |||||
return {'task_id': self.id, 'result': self._result, 'status': | |||||
self._state, 'traceback': self._traceback} | |||||
def __reduce__(self): | |||||
return self.__class__, self.__reduce_args__() | |||||
def __reduce_args__(self): | |||||
return (self.id, self._result, self._state, self._traceback) | |||||
def __copy__(self): | |||||
cls, args = self.__reduce__() | |||||
return cls(*args) | |||||
def ready(self): | |||||
return True | |||||
def get(self, timeout=None, propagate=True, **kwargs): | |||||
if self.successful(): | |||||
return self.result | |||||
elif self.state in states.PROPAGATE_STATES: | |||||
if propagate: | |||||
raise self.result | |||||
return self.result | |||||
wait = get | |||||
def forget(self): | |||||
pass | |||||
def revoke(self, *args, **kwargs): | |||||
self._state = states.REVOKED | |||||
def __repr__(self): | |||||
return '<EagerResult: {0.id}>'.format(self) | |||||
@property | |||||
def result(self): | |||||
"""The tasks return value""" | |||||
return self._result | |||||
@property | |||||
def state(self): | |||||
"""The tasks state.""" | |||||
return self._state | |||||
status = state | |||||
@property | |||||
def traceback(self): | |||||
"""The traceback if the task failed.""" | |||||
return self._traceback | |||||
@property | |||||
def supports_native_join(self): | |||||
return False | |||||
def result_from_tuple(r, app=None): | |||||
# earlier backends may just pickle, so check if | |||||
# result is already prepared. | |||||
app = app_or_default(app) | |||||
Result = app.AsyncResult | |||||
if not isinstance(r, ResultBase): | |||||
res, nodes = r | |||||
if nodes: | |||||
return app.GroupResult( | |||||
res, [result_from_tuple(child, app) for child in nodes], | |||||
) | |||||
# previously did not include parent | |||||
id, parent = res if isinstance(res, (list, tuple)) else (res, None) | |||||
if parent: | |||||
parent = result_from_tuple(parent, app) | |||||
return Result(id, parent=parent) | |||||
return r | |||||
from_serializable = result_from_tuple # XXX compat |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.schedules | |||||
~~~~~~~~~~~~~~~~ | |||||
Schedules define the intervals at which periodic tasks | |||||
should run. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import numbers | |||||
import re | |||||
from collections import namedtuple | |||||
from datetime import datetime, timedelta | |||||
from kombu.utils import cached_property | |||||
from . import current_app | |||||
from .five import range, string_t | |||||
from .utils import is_iterable | |||||
from .utils.timeutils import ( | |||||
timedelta_seconds, weekday, maybe_timedelta, remaining, | |||||
humanize_seconds, timezone, maybe_make_aware, ffwd | |||||
) | |||||
from .datastructures import AttributeDict | |||||
__all__ = ['ParseException', 'schedule', 'crontab', 'crontab_parser', | |||||
'maybe_schedule'] | |||||
schedstate = namedtuple('schedstate', ('is_due', 'next')) | |||||
CRON_PATTERN_INVALID = """\ | |||||
Invalid crontab pattern. Valid range is {min}-{max}. \ | |||||
'{value}' was found.\ | |||||
""" | |||||
CRON_INVALID_TYPE = """\ | |||||
Argument cronspec needs to be of any of the following types: \ | |||||
int, str, or an iterable type. {type!r} was given.\ | |||||
""" | |||||
CRON_REPR = """\ | |||||
<crontab: {0._orig_minute} {0._orig_hour} {0._orig_day_of_week} \ | |||||
{0._orig_day_of_month} {0._orig_month_of_year} (m/h/d/dM/MY)>\ | |||||
""" | |||||
def cronfield(s): | |||||
return '*' if s is None else s | |||||
class ParseException(Exception): | |||||
"""Raised by crontab_parser when the input can't be parsed.""" | |||||
class schedule(object): | |||||
"""Schedule for periodic task. | |||||
:param run_every: Interval in seconds (or a :class:`~datetime.timedelta`). | |||||
:param relative: If set to True the run time will be rounded to the | |||||
resolution of the interval. | |||||
:param nowfun: Function returning the current date and time | |||||
(class:`~datetime.datetime`). | |||||
:param app: Celery app instance. | |||||
""" | |||||
relative = False | |||||
def __init__(self, run_every=None, relative=False, nowfun=None, app=None): | |||||
self.run_every = maybe_timedelta(run_every) | |||||
self.relative = relative | |||||
self.nowfun = nowfun | |||||
self._app = app | |||||
def now(self): | |||||
return (self.nowfun or self.app.now)() | |||||
def remaining_estimate(self, last_run_at): | |||||
return remaining( | |||||
self.maybe_make_aware(last_run_at), self.run_every, | |||||
self.maybe_make_aware(self.now()), self.relative, | |||||
) | |||||
def is_due(self, last_run_at): | |||||
"""Returns tuple of two items `(is_due, next_time_to_check)`, | |||||
where next time to check is in seconds. | |||||
e.g. | |||||
* `(True, 20)`, means the task should be run now, and the next | |||||
time to check is in 20 seconds. | |||||
* `(False, 12.3)`, means the task is not due, but that the scheduler | |||||
should check again in 12.3 seconds. | |||||
The next time to check is used to save energy/cpu cycles, | |||||
it does not need to be accurate but will influence the precision | |||||
of your schedule. You must also keep in mind | |||||
the value of :setting:`CELERYBEAT_MAX_LOOP_INTERVAL`, | |||||
which decides the maximum number of seconds the scheduler can | |||||
sleep between re-checking the periodic task intervals. So if you | |||||
have a task that changes schedule at runtime then your next_run_at | |||||
check will decide how long it will take before a change to the | |||||
schedule takes effect. The max loop interval takes precendence | |||||
over the next check at value returned. | |||||
.. admonition:: Scheduler max interval variance | |||||
The default max loop interval may vary for different schedulers. | |||||
For the default scheduler the value is 5 minutes, but for e.g. | |||||
the django-celery database scheduler the value is 5 seconds. | |||||
""" | |||||
last_run_at = self.maybe_make_aware(last_run_at) | |||||
rem_delta = self.remaining_estimate(last_run_at) | |||||
remaining_s = timedelta_seconds(rem_delta) | |||||
if remaining_s == 0: | |||||
return schedstate(is_due=True, next=self.seconds) | |||||
return schedstate(is_due=False, next=remaining_s) | |||||
def maybe_make_aware(self, dt): | |||||
if self.utc_enabled: | |||||
return maybe_make_aware(dt, self.tz) | |||||
return dt | |||||
def __repr__(self): | |||||
return '<freq: {0.human_seconds}>'.format(self) | |||||
def __eq__(self, other): | |||||
if isinstance(other, schedule): | |||||
return self.run_every == other.run_every | |||||
return self.run_every == other | |||||
def __ne__(self, other): | |||||
return not self.__eq__(other) | |||||
def __reduce__(self): | |||||
return self.__class__, (self.run_every, self.relative, self.nowfun) | |||||
@property | |||||
def seconds(self): | |||||
return timedelta_seconds(self.run_every) | |||||
@property | |||||
def human_seconds(self): | |||||
return humanize_seconds(self.seconds) | |||||
@property | |||||
def app(self): | |||||
return self._app or current_app._get_current_object() | |||||
@app.setter # noqa | |||||
def app(self, app): | |||||
self._app = app | |||||
@cached_property | |||||
def tz(self): | |||||
return self.app.timezone | |||||
@cached_property | |||||
def utc_enabled(self): | |||||
return self.app.conf.CELERY_ENABLE_UTC | |||||
def to_local(self, dt): | |||||
if not self.utc_enabled: | |||||
return timezone.to_local_fallback(dt) | |||||
return dt | |||||
class crontab_parser(object): | |||||
"""Parser for crontab expressions. Any expression of the form 'groups' | |||||
(see BNF grammar below) is accepted and expanded to a set of numbers. | |||||
These numbers represent the units of time that the crontab needs to | |||||
run on:: | |||||
digit :: '0'..'9' | |||||
dow :: 'a'..'z' | |||||
number :: digit+ | dow+ | |||||
steps :: number | |||||
range :: number ( '-' number ) ? | |||||
numspec :: '*' | range | |||||
expr :: numspec ( '/' steps ) ? | |||||
groups :: expr ( ',' expr ) * | |||||
The parser is a general purpose one, useful for parsing hours, minutes and | |||||
day_of_week expressions. Example usage:: | |||||
>>> minutes = crontab_parser(60).parse('*/15') | |||||
[0, 15, 30, 45] | |||||
>>> hours = crontab_parser(24).parse('*/4') | |||||
[0, 4, 8, 12, 16, 20] | |||||
>>> day_of_week = crontab_parser(7).parse('*') | |||||
[0, 1, 2, 3, 4, 5, 6] | |||||
It can also parse day_of_month and month_of_year expressions if initialized | |||||
with an minimum of 1. Example usage:: | |||||
>>> days_of_month = crontab_parser(31, 1).parse('*/3') | |||||
[1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31] | |||||
>>> months_of_year = crontab_parser(12, 1).parse('*/2') | |||||
[1, 3, 5, 7, 9, 11] | |||||
>>> months_of_year = crontab_parser(12, 1).parse('2-12/2') | |||||
[2, 4, 6, 8, 10, 12] | |||||
The maximum possible expanded value returned is found by the formula:: | |||||
max_ + min_ - 1 | |||||
""" | |||||
ParseException = ParseException | |||||
_range = r'(\w+?)-(\w+)' | |||||
_steps = r'/(\w+)?' | |||||
_star = r'\*' | |||||
def __init__(self, max_=60, min_=0): | |||||
self.max_ = max_ | |||||
self.min_ = min_ | |||||
self.pats = ( | |||||
(re.compile(self._range + self._steps), self._range_steps), | |||||
(re.compile(self._range), self._expand_range), | |||||
(re.compile(self._star + self._steps), self._star_steps), | |||||
(re.compile('^' + self._star + '$'), self._expand_star), | |||||
) | |||||
def parse(self, spec): | |||||
acc = set() | |||||
for part in spec.split(','): | |||||
if not part: | |||||
raise self.ParseException('empty part') | |||||
acc |= set(self._parse_part(part)) | |||||
return acc | |||||
def _parse_part(self, part): | |||||
for regex, handler in self.pats: | |||||
m = regex.match(part) | |||||
if m: | |||||
return handler(m.groups()) | |||||
return self._expand_range((part, )) | |||||
def _expand_range(self, toks): | |||||
fr = self._expand_number(toks[0]) | |||||
if len(toks) > 1: | |||||
to = self._expand_number(toks[1]) | |||||
if to < fr: # Wrap around max_ if necessary | |||||
return (list(range(fr, self.min_ + self.max_)) + | |||||
list(range(self.min_, to + 1))) | |||||
return list(range(fr, to + 1)) | |||||
return [fr] | |||||
def _range_steps(self, toks): | |||||
if len(toks) != 3 or not toks[2]: | |||||
raise self.ParseException('empty filter') | |||||
return self._expand_range(toks[:2])[::int(toks[2])] | |||||
def _star_steps(self, toks): | |||||
if not toks or not toks[0]: | |||||
raise self.ParseException('empty filter') | |||||
return self._expand_star()[::int(toks[0])] | |||||
def _expand_star(self, *args): | |||||
return list(range(self.min_, self.max_ + self.min_)) | |||||
def _expand_number(self, s): | |||||
if isinstance(s, string_t) and s[0] == '-': | |||||
raise self.ParseException('negative numbers not supported') | |||||
try: | |||||
i = int(s) | |||||
except ValueError: | |||||
try: | |||||
i = weekday(s) | |||||
except KeyError: | |||||
raise ValueError('Invalid weekday literal {0!r}.'.format(s)) | |||||
max_val = self.min_ + self.max_ - 1 | |||||
if i > max_val: | |||||
raise ValueError( | |||||
'Invalid end range: {0} > {1}.'.format(i, max_val)) | |||||
if i < self.min_: | |||||
raise ValueError( | |||||
'Invalid beginning range: {0} < {1}.'.format(i, self.min_)) | |||||
return i | |||||
class crontab(schedule): | |||||
"""A crontab can be used as the `run_every` value of a | |||||
:class:`PeriodicTask` to add cron-like scheduling. | |||||
Like a :manpage:`cron` job, you can specify units of time of when | |||||
you would like the task to execute. It is a reasonably complete | |||||
implementation of cron's features, so it should provide a fair | |||||
degree of scheduling needs. | |||||
You can specify a minute, an hour, a day of the week, a day of the | |||||
month, and/or a month in the year in any of the following formats: | |||||
.. attribute:: minute | |||||
- A (list of) integers from 0-59 that represent the minutes of | |||||
an hour of when execution should occur; or | |||||
- A string representing a crontab pattern. This may get pretty | |||||
advanced, like `minute='*/15'` (for every quarter) or | |||||
`minute='1,13,30-45,50-59/2'`. | |||||
.. attribute:: hour | |||||
- A (list of) integers from 0-23 that represent the hours of | |||||
a day of when execution should occur; or | |||||
- A string representing a crontab pattern. This may get pretty | |||||
advanced, like `hour='*/3'` (for every three hours) or | |||||
`hour='0,8-17/2'` (at midnight, and every two hours during | |||||
office hours). | |||||
.. attribute:: day_of_week | |||||
- A (list of) integers from 0-6, where Sunday = 0 and Saturday = | |||||
6, that represent the days of a week that execution should | |||||
occur. | |||||
- A string representing a crontab pattern. This may get pretty | |||||
advanced, like `day_of_week='mon-fri'` (for weekdays only). | |||||
(Beware that `day_of_week='*/2'` does not literally mean | |||||
'every two days', but 'every day that is divisible by two'!) | |||||
.. attribute:: day_of_month | |||||
- A (list of) integers from 1-31 that represents the days of the | |||||
month that execution should occur. | |||||
- A string representing a crontab pattern. This may get pretty | |||||
advanced, such as `day_of_month='2-30/3'` (for every even | |||||
numbered day) or `day_of_month='1-7,15-21'` (for the first and | |||||
third weeks of the month). | |||||
.. attribute:: month_of_year | |||||
- A (list of) integers from 1-12 that represents the months of | |||||
the year during which execution can occur. | |||||
- A string representing a crontab pattern. This may get pretty | |||||
advanced, such as `month_of_year='*/3'` (for the first month | |||||
of every quarter) or `month_of_year='2-12/2'` (for every even | |||||
numbered month). | |||||
.. attribute:: nowfun | |||||
Function returning the current date and time | |||||
(:class:`~datetime.datetime`). | |||||
.. attribute:: app | |||||
The Celery app instance. | |||||
It is important to realize that any day on which execution should | |||||
occur must be represented by entries in all three of the day and | |||||
month attributes. For example, if `day_of_week` is 0 and `day_of_month` | |||||
is every seventh day, only months that begin on Sunday and are also | |||||
in the `month_of_year` attribute will have execution events. Or, | |||||
`day_of_week` is 1 and `day_of_month` is '1-7,15-21' means every | |||||
first and third monday of every month present in `month_of_year`. | |||||
""" | |||||
def __init__(self, minute='*', hour='*', day_of_week='*', | |||||
day_of_month='*', month_of_year='*', nowfun=None, app=None): | |||||
self._orig_minute = cronfield(minute) | |||||
self._orig_hour = cronfield(hour) | |||||
self._orig_day_of_week = cronfield(day_of_week) | |||||
self._orig_day_of_month = cronfield(day_of_month) | |||||
self._orig_month_of_year = cronfield(month_of_year) | |||||
self.hour = self._expand_cronspec(hour, 24) | |||||
self.minute = self._expand_cronspec(minute, 60) | |||||
self.day_of_week = self._expand_cronspec(day_of_week, 7) | |||||
self.day_of_month = self._expand_cronspec(day_of_month, 31, 1) | |||||
self.month_of_year = self._expand_cronspec(month_of_year, 12, 1) | |||||
self.nowfun = nowfun | |||||
self._app = app | |||||
@staticmethod | |||||
def _expand_cronspec(cronspec, max_, min_=0): | |||||
"""Takes the given cronspec argument in one of the forms:: | |||||
int (like 7) | |||||
str (like '3-5,*/15', '*', or 'monday') | |||||
set (like set([0,15,30,45])) | |||||
list (like [8-17]) | |||||
And convert it to an (expanded) set representing all time unit | |||||
values on which the crontab triggers. Only in case of the base | |||||
type being 'str', parsing occurs. (It is fast and | |||||
happens only once for each crontab instance, so there is no | |||||
significant performance overhead involved.) | |||||
For the other base types, merely Python type conversions happen. | |||||
The argument `max_` is needed to determine the expansion of '*' | |||||
and ranges. | |||||
The argument `min_` is needed to determine the expansion of '*' | |||||
and ranges for 1-based cronspecs, such as day of month or month | |||||
of year. The default is sufficient for minute, hour, and day of | |||||
week. | |||||
""" | |||||
if isinstance(cronspec, numbers.Integral): | |||||
result = set([cronspec]) | |||||
elif isinstance(cronspec, string_t): | |||||
result = crontab_parser(max_, min_).parse(cronspec) | |||||
elif isinstance(cronspec, set): | |||||
result = cronspec | |||||
elif is_iterable(cronspec): | |||||
result = set(cronspec) | |||||
else: | |||||
raise TypeError(CRON_INVALID_TYPE.format(type=type(cronspec))) | |||||
# assure the result does not preceed the min or exceed the max | |||||
for number in result: | |||||
if number >= max_ + min_ or number < min_: | |||||
raise ValueError(CRON_PATTERN_INVALID.format( | |||||
min=min_, max=max_ - 1 + min_, value=number)) | |||||
return result | |||||
def _delta_to_next(self, last_run_at, next_hour, next_minute): | |||||
""" | |||||
Takes a datetime of last run, next minute and hour, and | |||||
returns a relativedelta for the next scheduled day and time. | |||||
Only called when day_of_month and/or month_of_year cronspec | |||||
is specified to further limit scheduled task execution. | |||||
""" | |||||
from bisect import bisect, bisect_left | |||||
datedata = AttributeDict(year=last_run_at.year) | |||||
days_of_month = sorted(self.day_of_month) | |||||
months_of_year = sorted(self.month_of_year) | |||||
def day_out_of_range(year, month, day): | |||||
try: | |||||
datetime(year=year, month=month, day=day) | |||||
except ValueError: | |||||
return True | |||||
return False | |||||
def roll_over(): | |||||
while 1: | |||||
flag = (datedata.dom == len(days_of_month) or | |||||
day_out_of_range(datedata.year, | |||||
months_of_year[datedata.moy], | |||||
days_of_month[datedata.dom]) or | |||||
(self.maybe_make_aware(datetime(datedata.year, | |||||
months_of_year[datedata.moy], | |||||
days_of_month[datedata.dom])) < last_run_at)) | |||||
if flag: | |||||
datedata.dom = 0 | |||||
datedata.moy += 1 | |||||
if datedata.moy == len(months_of_year): | |||||
datedata.moy = 0 | |||||
datedata.year += 1 | |||||
else: | |||||
break | |||||
if last_run_at.month in self.month_of_year: | |||||
datedata.dom = bisect(days_of_month, last_run_at.day) | |||||
datedata.moy = bisect_left(months_of_year, last_run_at.month) | |||||
else: | |||||
datedata.dom = 0 | |||||
datedata.moy = bisect(months_of_year, last_run_at.month) | |||||
if datedata.moy == len(months_of_year): | |||||
datedata.moy = 0 | |||||
roll_over() | |||||
while 1: | |||||
th = datetime(year=datedata.year, | |||||
month=months_of_year[datedata.moy], | |||||
day=days_of_month[datedata.dom]) | |||||
if th.isoweekday() % 7 in self.day_of_week: | |||||
break | |||||
datedata.dom += 1 | |||||
roll_over() | |||||
return ffwd(year=datedata.year, | |||||
month=months_of_year[datedata.moy], | |||||
day=days_of_month[datedata.dom], | |||||
hour=next_hour, | |||||
minute=next_minute, | |||||
second=0, | |||||
microsecond=0) | |||||
def now(self): | |||||
return (self.nowfun or self.app.now)() | |||||
def __repr__(self): | |||||
return CRON_REPR.format(self) | |||||
def __reduce__(self): | |||||
return (self.__class__, (self._orig_minute, | |||||
self._orig_hour, | |||||
self._orig_day_of_week, | |||||
self._orig_day_of_month, | |||||
self._orig_month_of_year), None) | |||||
def remaining_delta(self, last_run_at, tz=None, ffwd=ffwd): | |||||
tz = tz or self.tz | |||||
last_run_at = self.maybe_make_aware(last_run_at) | |||||
now = self.maybe_make_aware(self.now()) | |||||
dow_num = last_run_at.isoweekday() % 7 # Sunday is day 0, not day 7 | |||||
execute_this_date = (last_run_at.month in self.month_of_year and | |||||
last_run_at.day in self.day_of_month and | |||||
dow_num in self.day_of_week) | |||||
execute_this_hour = (execute_this_date and | |||||
last_run_at.day == now.day and | |||||
last_run_at.month == now.month and | |||||
last_run_at.year == now.year and | |||||
last_run_at.hour in self.hour and | |||||
last_run_at.minute < max(self.minute)) | |||||
if execute_this_hour: | |||||
next_minute = min(minute for minute in self.minute | |||||
if minute > last_run_at.minute) | |||||
delta = ffwd(minute=next_minute, second=0, microsecond=0) | |||||
else: | |||||
next_minute = min(self.minute) | |||||
execute_today = (execute_this_date and | |||||
last_run_at.hour < max(self.hour)) | |||||
if execute_today: | |||||
next_hour = min(hour for hour in self.hour | |||||
if hour > last_run_at.hour) | |||||
delta = ffwd(hour=next_hour, minute=next_minute, | |||||
second=0, microsecond=0) | |||||
else: | |||||
next_hour = min(self.hour) | |||||
all_dom_moy = (self._orig_day_of_month == '*' and | |||||
self._orig_month_of_year == '*') | |||||
if all_dom_moy: | |||||
next_day = min([day for day in self.day_of_week | |||||
if day > dow_num] or self.day_of_week) | |||||
add_week = next_day == dow_num | |||||
delta = ffwd(weeks=add_week and 1 or 0, | |||||
weekday=(next_day - 1) % 7, | |||||
hour=next_hour, | |||||
minute=next_minute, | |||||
second=0, | |||||
microsecond=0) | |||||
else: | |||||
delta = self._delta_to_next(last_run_at, | |||||
next_hour, next_minute) | |||||
return self.to_local(last_run_at), delta, self.to_local(now) | |||||
def remaining_estimate(self, last_run_at, ffwd=ffwd): | |||||
"""Returns when the periodic task should run next as a timedelta.""" | |||||
return remaining(*self.remaining_delta(last_run_at, ffwd=ffwd)) | |||||
def is_due(self, last_run_at): | |||||
"""Returns tuple of two items `(is_due, next_time_to_run)`, | |||||
where next time to run is in seconds. | |||||
See :meth:`celery.schedules.schedule.is_due` for more information. | |||||
""" | |||||
rem_delta = self.remaining_estimate(last_run_at) | |||||
rem = timedelta_seconds(rem_delta) | |||||
due = rem == 0 | |||||
if due: | |||||
rem_delta = self.remaining_estimate(self.now()) | |||||
rem = timedelta_seconds(rem_delta) | |||||
return schedstate(due, rem) | |||||
def __eq__(self, other): | |||||
if isinstance(other, crontab): | |||||
return (other.month_of_year == self.month_of_year and | |||||
other.day_of_month == self.day_of_month and | |||||
other.day_of_week == self.day_of_week and | |||||
other.hour == self.hour and | |||||
other.minute == self.minute) | |||||
return NotImplemented | |||||
def __ne__(self, other): | |||||
return not self.__eq__(other) | |||||
def maybe_schedule(s, relative=False, app=None): | |||||
if s is not None: | |||||
if isinstance(s, numbers.Integral): | |||||
s = timedelta(seconds=s) | |||||
if isinstance(s, timedelta): | |||||
return schedule(s, relative, app=app) | |||||
else: | |||||
s.app = app | |||||
return s |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.security | |||||
~~~~~~~~~~~~~~~ | |||||
Module implementing the signing message serializer. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from kombu.serialization import ( | |||||
registry, disable_insecure_serializers as _disable_insecure_serializers, | |||||
) | |||||
from celery.exceptions import ImproperlyConfigured | |||||
from .serialization import register_auth | |||||
SSL_NOT_INSTALLED = """\ | |||||
You need to install the pyOpenSSL library to use the auth serializer. | |||||
Please install by: | |||||
$ pip install pyOpenSSL | |||||
""" | |||||
SETTING_MISSING = """\ | |||||
Sorry, but you have to configure the | |||||
* CELERY_SECURITY_KEY | |||||
* CELERY_SECURITY_CERTIFICATE, and the | |||||
* CELERY_SECURITY_CERT_STORE | |||||
configuration settings to use the auth serializer. | |||||
Please see the configuration reference for more information. | |||||
""" | |||||
__all__ = ['setup_security'] | |||||
def setup_security(allowed_serializers=None, key=None, cert=None, store=None, | |||||
digest='sha1', serializer='json', app=None): | |||||
"""See :meth:`@Celery.setup_security`.""" | |||||
if app is None: | |||||
from celery import current_app | |||||
app = current_app._get_current_object() | |||||
_disable_insecure_serializers(allowed_serializers) | |||||
conf = app.conf | |||||
if conf.CELERY_TASK_SERIALIZER != 'auth': | |||||
return | |||||
try: | |||||
from OpenSSL import crypto # noqa | |||||
except ImportError: | |||||
raise ImproperlyConfigured(SSL_NOT_INSTALLED) | |||||
key = key or conf.CELERY_SECURITY_KEY | |||||
cert = cert or conf.CELERY_SECURITY_CERTIFICATE | |||||
store = store or conf.CELERY_SECURITY_CERT_STORE | |||||
if not (key and cert and store): | |||||
raise ImproperlyConfigured(SETTING_MISSING) | |||||
with open(key) as kf: | |||||
with open(cert) as cf: | |||||
register_auth(kf.read(), cf.read(), store, digest, serializer) | |||||
registry._set_default_serializer('auth') | |||||
def disable_untrusted_serializers(whitelist=None): | |||||
_disable_insecure_serializers(allowed=whitelist) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.security.certificate | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
X.509 certificates. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import glob | |||||
import os | |||||
from kombu.utils.encoding import bytes_to_str | |||||
from celery.exceptions import SecurityError | |||||
from celery.five import values | |||||
from .utils import crypto, reraise_errors | |||||
__all__ = ['Certificate', 'CertStore', 'FSCertStore'] | |||||
class Certificate(object): | |||||
"""X.509 certificate.""" | |||||
def __init__(self, cert): | |||||
assert crypto is not None | |||||
with reraise_errors('Invalid certificate: {0!r}'): | |||||
self._cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) | |||||
def has_expired(self): | |||||
"""Check if the certificate has expired.""" | |||||
return self._cert.has_expired() | |||||
def get_serial_number(self): | |||||
"""Return the serial number in the certificate.""" | |||||
return bytes_to_str(self._cert.get_serial_number()) | |||||
def get_issuer(self): | |||||
"""Return issuer (CA) as a string""" | |||||
return ' '.join(bytes_to_str(x[1]) for x in | |||||
self._cert.get_issuer().get_components()) | |||||
def get_id(self): | |||||
"""Serial number/issuer pair uniquely identifies a certificate""" | |||||
return '{0} {1}'.format(self.get_issuer(), self.get_serial_number()) | |||||
def verify(self, data, signature, digest): | |||||
"""Verifies the signature for string containing data.""" | |||||
with reraise_errors('Bad signature: {0!r}'): | |||||
crypto.verify(self._cert, signature, data, digest) | |||||
class CertStore(object): | |||||
"""Base class for certificate stores""" | |||||
def __init__(self): | |||||
self._certs = {} | |||||
def itercerts(self): | |||||
"""an iterator over the certificates""" | |||||
for c in values(self._certs): | |||||
yield c | |||||
def __getitem__(self, id): | |||||
"""get certificate by id""" | |||||
try: | |||||
return self._certs[bytes_to_str(id)] | |||||
except KeyError: | |||||
raise SecurityError('Unknown certificate: {0!r}'.format(id)) | |||||
def add_cert(self, cert): | |||||
cert_id = bytes_to_str(cert.get_id()) | |||||
if cert_id in self._certs: | |||||
raise SecurityError('Duplicate certificate: {0!r}'.format(id)) | |||||
self._certs[cert_id] = cert | |||||
class FSCertStore(CertStore): | |||||
"""File system certificate store""" | |||||
def __init__(self, path): | |||||
CertStore.__init__(self) | |||||
if os.path.isdir(path): | |||||
path = os.path.join(path, '*') | |||||
for p in glob.glob(path): | |||||
with open(p) as f: | |||||
cert = Certificate(f.read()) | |||||
if cert.has_expired(): | |||||
raise SecurityError( | |||||
'Expired certificate: {0!r}'.format(cert.get_id())) | |||||
self.add_cert(cert) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.security.key | |||||
~~~~~~~~~~~~~~~~~~~ | |||||
Private key for the security serializer. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from kombu.utils.encoding import ensure_bytes | |||||
from .utils import crypto, reraise_errors | |||||
__all__ = ['PrivateKey'] | |||||
class PrivateKey(object): | |||||
def __init__(self, key): | |||||
with reraise_errors('Invalid private key: {0!r}'): | |||||
self._key = crypto.load_privatekey(crypto.FILETYPE_PEM, key) | |||||
def sign(self, data, digest): | |||||
"""sign string containing data.""" | |||||
with reraise_errors('Unable to sign data: {0!r}'): | |||||
return crypto.sign(self._key, ensure_bytes(data), digest) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.security.serialization | |||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |||||
Secure serializer. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import base64 | |||||
from kombu.serialization import registry, dumps, loads | |||||
from kombu.utils.encoding import bytes_to_str, str_to_bytes, ensure_bytes | |||||
from .certificate import Certificate, FSCertStore | |||||
from .key import PrivateKey | |||||
from .utils import reraise_errors | |||||
__all__ = ['SecureSerializer', 'register_auth'] | |||||
def b64encode(s): | |||||
return bytes_to_str(base64.b64encode(str_to_bytes(s))) | |||||
def b64decode(s): | |||||
return base64.b64decode(str_to_bytes(s)) | |||||
class SecureSerializer(object): | |||||
def __init__(self, key=None, cert=None, cert_store=None, | |||||
digest='sha1', serializer='json'): | |||||
self._key = key | |||||
self._cert = cert | |||||
self._cert_store = cert_store | |||||
self._digest = digest | |||||
self._serializer = serializer | |||||
def serialize(self, data): | |||||
"""serialize data structure into string""" | |||||
assert self._key is not None | |||||
assert self._cert is not None | |||||
with reraise_errors('Unable to serialize: {0!r}', (Exception, )): | |||||
content_type, content_encoding, body = dumps( | |||||
bytes_to_str(data), serializer=self._serializer) | |||||
# What we sign is the serialized body, not the body itself. | |||||
# this way the receiver doesn't have to decode the contents | |||||
# to verify the signature (and thus avoiding potential flaws | |||||
# in the decoding step). | |||||
body = ensure_bytes(body) | |||||
return self._pack(body, content_type, content_encoding, | |||||
signature=self._key.sign(body, self._digest), | |||||
signer=self._cert.get_id()) | |||||
def deserialize(self, data): | |||||
"""deserialize data structure from string""" | |||||
assert self._cert_store is not None | |||||
with reraise_errors('Unable to deserialize: {0!r}', (Exception, )): | |||||
payload = self._unpack(data) | |||||
signature, signer, body = (payload['signature'], | |||||
payload['signer'], | |||||
payload['body']) | |||||
self._cert_store[signer].verify(body, signature, self._digest) | |||||
return loads(bytes_to_str(body), payload['content_type'], | |||||
payload['content_encoding'], force=True) | |||||
def _pack(self, body, content_type, content_encoding, signer, signature, | |||||
sep=str_to_bytes('\x00\x01')): | |||||
fields = sep.join( | |||||
ensure_bytes(s) for s in [signer, signature, content_type, | |||||
content_encoding, body] | |||||
) | |||||
return b64encode(fields) | |||||
def _unpack(self, payload, sep=str_to_bytes('\x00\x01')): | |||||
raw_payload = b64decode(ensure_bytes(payload)) | |||||
first_sep = raw_payload.find(sep) | |||||
signer = raw_payload[:first_sep] | |||||
signer_cert = self._cert_store[signer] | |||||
sig_len = signer_cert._cert.get_pubkey().bits() >> 3 | |||||
signature = raw_payload[ | |||||
first_sep + len(sep):first_sep + len(sep) + sig_len | |||||
] | |||||
end_of_sig = first_sep + len(sep) + sig_len + len(sep) | |||||
v = raw_payload[end_of_sig:].split(sep) | |||||
return { | |||||
'signer': signer, | |||||
'signature': signature, | |||||
'content_type': bytes_to_str(v[0]), | |||||
'content_encoding': bytes_to_str(v[1]), | |||||
'body': bytes_to_str(v[2]), | |||||
} | |||||
def register_auth(key=None, cert=None, store=None, digest='sha1', | |||||
serializer='json'): | |||||
"""register security serializer""" | |||||
s = SecureSerializer(key and PrivateKey(key), | |||||
cert and Certificate(cert), | |||||
store and FSCertStore(store), | |||||
digest=digest, serializer=serializer) | |||||
registry.register('auth', s.serialize, s.deserialize, | |||||
content_type='application/data', | |||||
content_encoding='utf-8') |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.security.utils | |||||
~~~~~~~~~~~~~~~~~~~~~ | |||||
Utilities used by the message signing serializer. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import sys | |||||
from contextlib import contextmanager | |||||
from celery.exceptions import SecurityError | |||||
from celery.five import reraise | |||||
try: | |||||
from OpenSSL import crypto | |||||
except ImportError: # pragma: no cover | |||||
crypto = None # noqa | |||||
__all__ = ['reraise_errors'] | |||||
@contextmanager | |||||
def reraise_errors(msg='{0!r}', errors=None): | |||||
assert crypto is not None | |||||
errors = (crypto.Error, ) if errors is None else errors | |||||
try: | |||||
yield | |||||
except errors as exc: | |||||
reraise(SecurityError, | |||||
SecurityError(msg.format(exc)), | |||||
sys.exc_info()[2]) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.signals | |||||
~~~~~~~~~~~~~~ | |||||
This module defines the signals (Observer pattern) sent by | |||||
both workers and clients. | |||||
Functions can be connected to these signals, and connected | |||||
functions are called whenever a signal is called. | |||||
See :ref:`signals` for more information. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from .utils.dispatch import Signal | |||||
__all__ = ['before_task_publish', 'after_task_publish', | |||||
'task_prerun', 'task_postrun', 'task_success', | |||||
'task_retry', 'task_failure', 'task_revoked', 'celeryd_init', | |||||
'celeryd_after_setup', 'worker_init', 'worker_process_init', | |||||
'worker_ready', 'worker_shutdown', 'setup_logging', | |||||
'after_setup_logger', 'after_setup_task_logger', | |||||
'beat_init', 'beat_embedded_init', 'eventlet_pool_started', | |||||
'eventlet_pool_preshutdown', 'eventlet_pool_postshutdown', | |||||
'eventlet_pool_apply'] | |||||
before_task_publish = Signal(providing_args=[ | |||||
'body', 'exchange', 'routing_key', 'headers', 'properties', | |||||
'declare', 'retry_policy', | |||||
]) | |||||
after_task_publish = Signal(providing_args=[ | |||||
'body', 'exchange', 'routing_key', | |||||
]) | |||||
#: Deprecated, use after_task_publish instead. | |||||
task_sent = Signal(providing_args=[ | |||||
'task_id', 'task', 'args', 'kwargs', 'eta', 'taskset', | |||||
]) | |||||
task_prerun = Signal(providing_args=['task_id', 'task', 'args', 'kwargs']) | |||||
task_postrun = Signal(providing_args=[ | |||||
'task_id', 'task', 'args', 'kwargs', 'retval', | |||||
]) | |||||
task_success = Signal(providing_args=['result']) | |||||
task_retry = Signal(providing_args=[ | |||||
'request', 'reason', 'einfo', | |||||
]) | |||||
task_failure = Signal(providing_args=[ | |||||
'task_id', 'exception', 'args', 'kwargs', 'traceback', 'einfo', | |||||
]) | |||||
task_revoked = Signal(providing_args=[ | |||||
'request', 'terminated', 'signum', 'expired', | |||||
]) | |||||
celeryd_init = Signal(providing_args=['instance', 'conf', 'options']) | |||||
celeryd_after_setup = Signal(providing_args=['instance', 'conf']) | |||||
import_modules = Signal(providing_args=[]) | |||||
worker_init = Signal(providing_args=[]) | |||||
worker_process_init = Signal(providing_args=[]) | |||||
worker_process_shutdown = Signal(providing_args=[]) | |||||
worker_ready = Signal(providing_args=[]) | |||||
worker_shutdown = Signal(providing_args=[]) | |||||
setup_logging = Signal(providing_args=[ | |||||
'loglevel', 'logfile', 'format', 'colorize', | |||||
]) | |||||
after_setup_logger = Signal(providing_args=[ | |||||
'logger', 'loglevel', 'logfile', 'format', 'colorize', | |||||
]) | |||||
after_setup_task_logger = Signal(providing_args=[ | |||||
'logger', 'loglevel', 'logfile', 'format', 'colorize', | |||||
]) | |||||
beat_init = Signal(providing_args=[]) | |||||
beat_embedded_init = Signal(providing_args=[]) | |||||
eventlet_pool_started = Signal(providing_args=[]) | |||||
eventlet_pool_preshutdown = Signal(providing_args=[]) | |||||
eventlet_pool_postshutdown = Signal(providing_args=[]) | |||||
eventlet_pool_apply = Signal(providing_args=['target', 'args', 'kwargs']) | |||||
user_preload_options = Signal(providing_args=['app', 'options']) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.states | |||||
============= | |||||
Built-in task states. | |||||
.. _states: | |||||
States | |||||
------ | |||||
See :ref:`task-states`. | |||||
.. _statesets: | |||||
Sets | |||||
---- | |||||
.. state:: READY_STATES | |||||
READY_STATES | |||||
~~~~~~~~~~~~ | |||||
Set of states meaning the task result is ready (has been executed). | |||||
.. state:: UNREADY_STATES | |||||
UNREADY_STATES | |||||
~~~~~~~~~~~~~~ | |||||
Set of states meaning the task result is not ready (has not been executed). | |||||
.. state:: EXCEPTION_STATES | |||||
EXCEPTION_STATES | |||||
~~~~~~~~~~~~~~~~ | |||||
Set of states meaning the task returned an exception. | |||||
.. state:: PROPAGATE_STATES | |||||
PROPAGATE_STATES | |||||
~~~~~~~~~~~~~~~~ | |||||
Set of exception states that should propagate exceptions to the user. | |||||
.. state:: ALL_STATES | |||||
ALL_STATES | |||||
~~~~~~~~~~ | |||||
Set of all possible states. | |||||
Misc. | |||||
----- | |||||
""" | |||||
from __future__ import absolute_import | |||||
__all__ = ['PENDING', 'RECEIVED', 'STARTED', 'SUCCESS', 'FAILURE', | |||||
'REVOKED', 'RETRY', 'IGNORED', 'READY_STATES', 'UNREADY_STATES', | |||||
'EXCEPTION_STATES', 'PROPAGATE_STATES', 'precedence', 'state'] | |||||
#: State precedence. | |||||
#: None represents the precedence of an unknown state. | |||||
#: Lower index means higher precedence. | |||||
PRECEDENCE = ['SUCCESS', | |||||
'FAILURE', | |||||
None, | |||||
'REVOKED', | |||||
'STARTED', | |||||
'RECEIVED', | |||||
'RETRY', | |||||
'PENDING'] | |||||
#: Hash lookup of PRECEDENCE to index | |||||
PRECEDENCE_LOOKUP = dict(zip(PRECEDENCE, range(0, len(PRECEDENCE)))) | |||||
NONE_PRECEDENCE = PRECEDENCE_LOOKUP[None] | |||||
def precedence(state): | |||||
"""Get the precedence index for state. | |||||
Lower index means higher precedence. | |||||
""" | |||||
try: | |||||
return PRECEDENCE_LOOKUP[state] | |||||
except KeyError: | |||||
return NONE_PRECEDENCE | |||||
class state(str): | |||||
"""State is a subclass of :class:`str`, implementing comparison | |||||
methods adhering to state precedence rules:: | |||||
>>> from celery.states import state, PENDING, SUCCESS | |||||
>>> state(PENDING) < state(SUCCESS) | |||||
True | |||||
Any custom state is considered to be lower than :state:`FAILURE` and | |||||
:state:`SUCCESS`, but higher than any of the other built-in states:: | |||||
>>> state('PROGRESS') > state(STARTED) | |||||
True | |||||
>>> state('PROGRESS') > state('SUCCESS') | |||||
False | |||||
""" | |||||
def compare(self, other, fun): | |||||
return fun(precedence(self), precedence(other)) | |||||
def __gt__(self, other): | |||||
return precedence(self) < precedence(other) | |||||
def __ge__(self, other): | |||||
return precedence(self) <= precedence(other) | |||||
def __lt__(self, other): | |||||
return precedence(self) > precedence(other) | |||||
def __le__(self, other): | |||||
return precedence(self) >= precedence(other) | |||||
#: Task state is unknown (assumed pending since you know the id). | |||||
PENDING = 'PENDING' | |||||
#: Task was received by a worker. | |||||
RECEIVED = 'RECEIVED' | |||||
#: Task was started by a worker (:setting:`CELERY_TRACK_STARTED`). | |||||
STARTED = 'STARTED' | |||||
#: Task succeeded | |||||
SUCCESS = 'SUCCESS' | |||||
#: Task failed | |||||
FAILURE = 'FAILURE' | |||||
#: Task was revoked. | |||||
REVOKED = 'REVOKED' | |||||
#: Task is waiting for retry. | |||||
RETRY = 'RETRY' | |||||
IGNORED = 'IGNORED' | |||||
REJECTED = 'REJECTED' | |||||
READY_STATES = frozenset([SUCCESS, FAILURE, REVOKED]) | |||||
UNREADY_STATES = frozenset([PENDING, RECEIVED, STARTED, RETRY]) | |||||
EXCEPTION_STATES = frozenset([RETRY, FAILURE, REVOKED]) | |||||
PROPAGATE_STATES = frozenset([FAILURE, REVOKED]) | |||||
ALL_STATES = frozenset([PENDING, RECEIVED, STARTED, | |||||
SUCCESS, FAILURE, RETRY, REVOKED]) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.task | |||||
~~~~~~~~~~~ | |||||
This is the old task module, it should not be used anymore, | |||||
import from the main 'celery' module instead. | |||||
If you're looking for the decorator implementation then that's in | |||||
``celery.app.base.Celery.task``. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from celery._state import current_app, current_task as current | |||||
from celery.five import LazyModule, recreate_module | |||||
from celery.local import Proxy | |||||
__all__ = [ | |||||
'BaseTask', 'Task', 'PeriodicTask', 'task', 'periodic_task', | |||||
'group', 'chord', 'subtask', 'TaskSet', | |||||
] | |||||
STATICA_HACK = True | |||||
globals()['kcah_acitats'[::-1].upper()] = False | |||||
if STATICA_HACK: # pragma: no cover | |||||
# This is never executed, but tricks static analyzers (PyDev, PyCharm, | |||||
# pylint, etc.) into knowing the types of these symbols, and what | |||||
# they contain. | |||||
from celery.canvas import group, chord, subtask | |||||
from .base import BaseTask, Task, PeriodicTask, task, periodic_task | |||||
from .sets import TaskSet | |||||
class module(LazyModule): | |||||
def __call__(self, *args, **kwargs): | |||||
return self.task(*args, **kwargs) | |||||
old_module, new_module = recreate_module( # pragma: no cover | |||||
__name__, | |||||
by_module={ | |||||
'celery.task.base': ['BaseTask', 'Task', 'PeriodicTask', | |||||
'task', 'periodic_task'], | |||||
'celery.canvas': ['group', 'chord', 'subtask'], | |||||
'celery.task.sets': ['TaskSet'], | |||||
}, | |||||
base=module, | |||||
__package__='celery.task', | |||||
__file__=__file__, | |||||
__path__=__path__, | |||||
__doc__=__doc__, | |||||
current=current, | |||||
discard_all=Proxy(lambda: current_app.control.purge), | |||||
backend_cleanup=Proxy( | |||||
lambda: current_app.tasks['celery.backend_cleanup'] | |||||
), | |||||
) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.task.base | |||||
~~~~~~~~~~~~~~~~ | |||||
The task implementation has been moved to :mod:`celery.app.task`. | |||||
This contains the backward compatible Task class used in the old API, | |||||
and shouldn't be used in new applications. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from kombu import Exchange | |||||
from celery import current_app | |||||
from celery.app.task import Context, TaskType, Task as BaseTask # noqa | |||||
from celery.five import class_property, reclassmethod | |||||
from celery.schedules import maybe_schedule | |||||
from celery.utils.log import get_task_logger | |||||
__all__ = ['Task', 'PeriodicTask', 'task'] | |||||
#: list of methods that must be classmethods in the old API. | |||||
_COMPAT_CLASSMETHODS = ( | |||||
'delay', 'apply_async', 'retry', 'apply', 'subtask_from_request', | |||||
'AsyncResult', 'subtask', '_get_request', '_get_exec_options', | |||||
) | |||||
class Task(BaseTask): | |||||
"""Deprecated Task base class. | |||||
Modern applications should use :class:`celery.Task` instead. | |||||
""" | |||||
abstract = True | |||||
__bound__ = False | |||||
__v2_compat__ = True | |||||
# - Deprecated compat. attributes -: | |||||
queue = None | |||||
routing_key = None | |||||
exchange = None | |||||
exchange_type = None | |||||
delivery_mode = None | |||||
mandatory = False # XXX deprecated | |||||
immediate = False # XXX deprecated | |||||
priority = None | |||||
type = 'regular' | |||||
disable_error_emails = False | |||||
accept_magic_kwargs = False | |||||
from_config = BaseTask.from_config + ( | |||||
('exchange_type', 'CELERY_DEFAULT_EXCHANGE_TYPE'), | |||||
('delivery_mode', 'CELERY_DEFAULT_DELIVERY_MODE'), | |||||
) | |||||
# In old Celery the @task decorator didn't exist, so one would create | |||||
# classes instead and use them directly (e.g. MyTask.apply_async()). | |||||
# the use of classmethods was a hack so that it was not necessary | |||||
# to instantiate the class before using it, but it has only | |||||
# given us pain (like all magic). | |||||
for name in _COMPAT_CLASSMETHODS: | |||||
locals()[name] = reclassmethod(getattr(BaseTask, name)) | |||||
@class_property | |||||
def request(cls): | |||||
return cls._get_request() | |||||
@class_property | |||||
def backend(cls): | |||||
if cls._backend is None: | |||||
return cls.app.backend | |||||
return cls._backend | |||||
@backend.setter | |||||
def backend(cls, value): # noqa | |||||
cls._backend = value | |||||
@classmethod | |||||
def get_logger(self, **kwargs): | |||||
return get_task_logger(self.name) | |||||
@classmethod | |||||
def establish_connection(self): | |||||
"""Deprecated method used to get a broker connection. | |||||
Should be replaced with :meth:`@Celery.connection` | |||||
instead, or by acquiring connections from the connection pool: | |||||
.. code-block:: python | |||||
# using the connection pool | |||||
with celery.pool.acquire(block=True) as conn: | |||||
... | |||||
# establish fresh connection | |||||
with celery.connection() as conn: | |||||
... | |||||
""" | |||||
return self._get_app().connection() | |||||
def get_publisher(self, connection=None, exchange=None, | |||||
exchange_type=None, **options): | |||||
"""Deprecated method to get the task publisher (now called producer). | |||||
Should be replaced with :class:`@amqp.TaskProducer`: | |||||
.. code-block:: python | |||||
with celery.connection() as conn: | |||||
with celery.amqp.TaskProducer(conn) as prod: | |||||
my_task.apply_async(producer=prod) | |||||
""" | |||||
exchange = self.exchange if exchange is None else exchange | |||||
if exchange_type is None: | |||||
exchange_type = self.exchange_type | |||||
connection = connection or self.establish_connection() | |||||
return self._get_app().amqp.TaskProducer( | |||||
connection, | |||||
exchange=exchange and Exchange(exchange, exchange_type), | |||||
routing_key=self.routing_key, **options | |||||
) | |||||
@classmethod | |||||
def get_consumer(self, connection=None, queues=None, **kwargs): | |||||
"""Deprecated method used to get consumer for the queue | |||||
this task is sent to. | |||||
Should be replaced with :class:`@amqp.TaskConsumer` instead: | |||||
""" | |||||
Q = self._get_app().amqp | |||||
connection = connection or self.establish_connection() | |||||
if queues is None: | |||||
queues = Q.queues[self.queue] if self.queue else Q.default_queue | |||||
return Q.TaskConsumer(connection, queues, **kwargs) | |||||
class PeriodicTask(Task): | |||||
"""A periodic task is a task that adds itself to the | |||||
:setting:`CELERYBEAT_SCHEDULE` setting.""" | |||||
abstract = True | |||||
ignore_result = True | |||||
relative = False | |||||
options = None | |||||
compat = True | |||||
def __init__(self): | |||||
if not hasattr(self, 'run_every'): | |||||
raise NotImplementedError( | |||||
'Periodic tasks must have a run_every attribute') | |||||
self.run_every = maybe_schedule(self.run_every, self.relative) | |||||
super(PeriodicTask, self).__init__() | |||||
@classmethod | |||||
def on_bound(cls, app): | |||||
app.conf.CELERYBEAT_SCHEDULE[cls.name] = { | |||||
'task': cls.name, | |||||
'schedule': cls.run_every, | |||||
'args': (), | |||||
'kwargs': {}, | |||||
'options': cls.options or {}, | |||||
'relative': cls.relative, | |||||
} | |||||
def task(*args, **kwargs): | |||||
"""Deprecated decorator, please use :func:`celery.task`.""" | |||||
return current_app.task(*args, **dict({'accept_magic_kwargs': False, | |||||
'base': Task}, **kwargs)) | |||||
def periodic_task(*args, **options): | |||||
"""Deprecated decorator, please use :setting:`CELERYBEAT_SCHEDULE`.""" | |||||
return task(**dict({'base': PeriodicTask}, **options)) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.task.http | |||||
~~~~~~~~~~~~~~~~ | |||||
Webhook task implementation. | |||||
""" | |||||
from __future__ import absolute_import | |||||
import anyjson | |||||
import sys | |||||
try: | |||||
from urllib.parse import parse_qsl, urlencode, urlparse # Py3 | |||||
except ImportError: # pragma: no cover | |||||
from urllib import urlencode # noqa | |||||
from urlparse import urlparse, parse_qsl # noqa | |||||
from celery import shared_task, __version__ as celery_version | |||||
from celery.five import items, reraise | |||||
from celery.utils.log import get_task_logger | |||||
__all__ = ['InvalidResponseError', 'RemoteExecuteError', 'UnknownStatusError', | |||||
'HttpDispatch', 'dispatch', 'URL'] | |||||
GET_METHODS = frozenset(['GET', 'HEAD']) | |||||
logger = get_task_logger(__name__) | |||||
if sys.version_info[0] == 3: # pragma: no cover | |||||
from urllib.request import Request, urlopen | |||||
def utf8dict(tup): | |||||
if not isinstance(tup, dict): | |||||
return dict(tup) | |||||
return tup | |||||
else: | |||||
from urllib2 import Request, urlopen # noqa | |||||
def utf8dict(tup): # noqa | |||||
"""With a dict's items() tuple return a new dict with any utf-8 | |||||
keys/values encoded.""" | |||||
return dict( | |||||
(k.encode('utf-8'), | |||||
v.encode('utf-8') if isinstance(v, unicode) else v) # noqa | |||||
for k, v in tup) | |||||
class InvalidResponseError(Exception): | |||||
"""The remote server gave an invalid response.""" | |||||
class RemoteExecuteError(Exception): | |||||
"""The remote task gave a custom error.""" | |||||
class UnknownStatusError(InvalidResponseError): | |||||
"""The remote server gave an unknown status.""" | |||||
def extract_response(raw_response, loads=anyjson.loads): | |||||
"""Extract the response text from a raw JSON response.""" | |||||
if not raw_response: | |||||
raise InvalidResponseError('Empty response') | |||||
try: | |||||
payload = loads(raw_response) | |||||
except ValueError as exc: | |||||
reraise(InvalidResponseError, InvalidResponseError( | |||||
str(exc)), sys.exc_info()[2]) | |||||
status = payload['status'] | |||||
if status == 'success': | |||||
return payload['retval'] | |||||
elif status == 'failure': | |||||
raise RemoteExecuteError(payload.get('reason')) | |||||
else: | |||||
raise UnknownStatusError(str(status)) | |||||
class MutableURL(object): | |||||
"""Object wrapping a Uniform Resource Locator. | |||||
Supports editing the query parameter list. | |||||
You can convert the object back to a string, the query will be | |||||
properly urlencoded. | |||||
Examples | |||||
>>> url = URL('http://www.google.com:6580/foo/bar?x=3&y=4#foo') | |||||
>>> url.query | |||||
{'x': '3', 'y': '4'} | |||||
>>> str(url) | |||||
'http://www.google.com:6580/foo/bar?y=4&x=3#foo' | |||||
>>> url.query['x'] = 10 | |||||
>>> url.query.update({'George': 'Costanza'}) | |||||
>>> str(url) | |||||
'http://www.google.com:6580/foo/bar?y=4&x=10&George=Costanza#foo' | |||||
""" | |||||
def __init__(self, url): | |||||
self.parts = urlparse(url) | |||||
self.query = dict(parse_qsl(self.parts[4])) | |||||
def __str__(self): | |||||
scheme, netloc, path, params, query, fragment = self.parts | |||||
query = urlencode(utf8dict(items(self.query))) | |||||
components = [scheme + '://', netloc, path or '/', | |||||
';{0}'.format(params) if params else '', | |||||
'?{0}'.format(query) if query else '', | |||||
'#{0}'.format(fragment) if fragment else ''] | |||||
return ''.join(c for c in components if c) | |||||
def __repr__(self): | |||||
return '<{0}: {1}>'.format(type(self).__name__, self) | |||||
class HttpDispatch(object): | |||||
"""Make task HTTP request and collect the task result. | |||||
:param url: The URL to request. | |||||
:param method: HTTP method used. Currently supported methods are `GET` | |||||
and `POST`. | |||||
:param task_kwargs: Task keyword arguments. | |||||
:param logger: Logger used for user/system feedback. | |||||
""" | |||||
user_agent = 'celery/{version}'.format(version=celery_version) | |||||
timeout = 5 | |||||
def __init__(self, url, method, task_kwargs, **kwargs): | |||||
self.url = url | |||||
self.method = method | |||||
self.task_kwargs = task_kwargs | |||||
self.logger = kwargs.get('logger') or logger | |||||
def make_request(self, url, method, params): | |||||
"""Perform HTTP request and return the response.""" | |||||
request = Request(url, params) | |||||
for key, val in items(self.http_headers): | |||||
request.add_header(key, val) | |||||
response = urlopen(request) # user catches errors. | |||||
return response.read() | |||||
def dispatch(self): | |||||
"""Dispatch callback and return result.""" | |||||
url = MutableURL(self.url) | |||||
params = None | |||||
if self.method in GET_METHODS: | |||||
url.query.update(self.task_kwargs) | |||||
else: | |||||
params = urlencode(utf8dict(items(self.task_kwargs))) | |||||
raw_response = self.make_request(str(url), self.method, params) | |||||
return extract_response(raw_response) | |||||
@property | |||||
def http_headers(self): | |||||
headers = {'User-Agent': self.user_agent} | |||||
return headers | |||||
@shared_task(name='celery.http_dispatch', bind=True, | |||||
url=None, method=None, accept_magic_kwargs=False) | |||||
def dispatch(self, url=None, method='GET', **kwargs): | |||||
"""Task dispatching to an URL. | |||||
:keyword url: The URL location of the HTTP callback task. | |||||
:keyword method: Method to use when dispatching the callback. Usually | |||||
`GET` or `POST`. | |||||
:keyword \*\*kwargs: Keyword arguments to pass on to the HTTP callback. | |||||
.. attribute:: url | |||||
If this is set, this is used as the default URL for requests. | |||||
Default is to require the user of the task to supply the url as an | |||||
argument, as this attribute is intended for subclasses. | |||||
.. attribute:: method | |||||
If this is set, this is the default method used for requests. | |||||
Default is to require the user of the task to supply the method as an | |||||
argument, as this attribute is intended for subclasses. | |||||
""" | |||||
return HttpDispatch( | |||||
url or self.url, method or self.method, kwargs, | |||||
).dispatch() | |||||
class URL(MutableURL): | |||||
"""HTTP Callback URL | |||||
Supports requesting an URL asynchronously. | |||||
:param url: URL to request. | |||||
:keyword dispatcher: Class used to dispatch the request. | |||||
By default this is :func:`dispatch`. | |||||
""" | |||||
dispatcher = None | |||||
def __init__(self, url, dispatcher=None, app=None): | |||||
super(URL, self).__init__(url) | |||||
self.app = app | |||||
self.dispatcher = dispatcher or self.dispatcher | |||||
if self.dispatcher is None: | |||||
# Get default dispatcher | |||||
self.dispatcher = ( | |||||
self.app.tasks['celery.http_dispatch'] if self.app | |||||
else dispatch | |||||
) | |||||
def get_async(self, **kwargs): | |||||
return self.dispatcher.delay(str(self), 'GET', **kwargs) | |||||
def post_async(self, **kwargs): | |||||
return self.dispatcher.delay(str(self), 'POST', **kwargs) |
# -*- coding: utf-8 -*- | |||||
""" | |||||
celery.task.sets | |||||
~~~~~~~~~~~~~~~~ | |||||
Old ``group`` implementation, this module should | |||||
not be used anymore use :func:`celery.group` instead. | |||||
""" | |||||
from __future__ import absolute_import | |||||
from celery._state import get_current_worker_task | |||||
from celery.app import app_or_default | |||||
from celery.canvas import maybe_signature # noqa | |||||
from celery.utils import uuid, warn_deprecated | |||||
from celery.canvas import subtask # noqa | |||||
warn_deprecated( | |||||
'celery.task.sets and TaskSet', removal='4.0', | |||||
alternative="""\ | |||||
Please use "group" instead (see the Canvas section in the userguide)\ | |||||
""") | |||||
class TaskSet(list): | |||||
"""A task containing several subtasks, making it possible | |||||
to track how many, or when all of the tasks have been completed. | |||||
:param tasks: A list of :class:`subtask` instances. | |||||
Example:: | |||||
>>> from myproj.tasks import refresh_feed | |||||
>>> urls = ('http://cnn.com/rss', 'http://bbc.co.uk/rss') | |||||
>>> s = TaskSet(refresh_feed.s(url) for url in urls) | |||||
>>> taskset_result = s.apply_async() | |||||
>>> list_of_return_values = taskset_result.join() # *expensive* | |||||
""" | |||||
app = None | |||||
def __init__(self, tasks=None, app=None, Publisher=None): | |||||
self.app = app_or_default(app or self.app) | |||||
super(TaskSet, self).__init__( | |||||
maybe_signature(t, app=self.app) for t in tasks or [] | |||||
) | |||||
self.Publisher = Publisher or self.app.amqp.TaskProducer | |||||
self.total = len(self) # XXX compat | |||||
def apply_async(self, connection=None, publisher=None, taskset_id=None): | |||||
"""Apply TaskSet.""" | |||||
app = self.app | |||||
if app.conf.CELERY_ALWAYS_EAGER: | |||||
return self.apply(taskset_id=taskset_id) | |||||
with app.connection_or_acquire(connection) as conn: | |||||
setid = taskset_id or uuid() | |||||
pub = publisher or self.Publisher(conn) | |||||
results = self._async_results(setid, pub) | |||||
result = app.TaskSetResult(setid, results) | |||||
parent = get_current_worker_task() | |||||
if parent: | |||||
parent.add_trail(result) | |||||
return result | |||||
def _async_results(self, taskset_id, publisher): | |||||
return [task.apply_async(taskset_id=taskset_id, publisher=publisher) | |||||
for task in self] | |||||
def apply(self, taskset_id=None): | |||||
"""Applies the TaskSet locally by blocking until all tasks return.""" | |||||
setid = taskset_id or uuid() | |||||
return self.app.TaskSetResult(setid, self._sync_results(setid)) | |||||
def _sync_results(self, taskset_id): | |||||
return [task.apply(taskset_id=taskset_id) for task in self] | |||||
@property | |||||
def tasks(self): | |||||
return self | |||||
@tasks.setter # noqa | |||||
def tasks(self, tasks): | |||||
self[:] = tasks |
"""This module has moved to celery.app.trace.""" | |||||
from __future__ import absolute_import | |||||
import sys | |||||
from celery.app import trace | |||||
from celery.utils import warn_deprecated | |||||
warn_deprecated('celery.task.trace', removal='3.2', | |||||
alternative='Please use celery.app.trace instead.') | |||||
sys.modules[__name__] = trace |
from __future__ import absolute_import | |||||
import logging | |||||
import os | |||||
import sys | |||||
import warnings | |||||
from importlib import import_module | |||||
try: | |||||
WindowsError = WindowsError # noqa | |||||
except NameError: | |||||
class WindowsError(Exception): | |||||
pass | |||||
def setup(): | |||||
os.environ.update( | |||||
# warn if config module not found | |||||
C_WNOCONF='yes', | |||||
KOMBU_DISABLE_LIMIT_PROTECTION='yes', | |||||
) | |||||
if os.environ.get('COVER_ALL_MODULES') or '--with-coverage' in sys.argv: | |||||
from warnings import catch_warnings | |||||
with catch_warnings(record=True): | |||||
import_all_modules() | |||||
warnings.resetwarnings() | |||||
from celery.tests.case import Trap | |||||
from celery._state import set_default_app | |||||
set_default_app(Trap()) | |||||
def teardown(): | |||||
# Don't want SUBDEBUG log messages at finalization. | |||||
try: | |||||
from multiprocessing.util import get_logger | |||||
except ImportError: | |||||
pass | |||||
else: | |||||
get_logger().setLevel(logging.WARNING) | |||||
# Make sure test database is removed. | |||||
import os | |||||
if os.path.exists('test.db'): | |||||
try: | |||||
os.remove('test.db') | |||||
except WindowsError: | |||||
pass | |||||
# Make sure there are no remaining threads at shutdown. | |||||
import threading | |||||
remaining_threads = [thread for thread in threading.enumerate() | |||||
if thread.getName() != 'MainThread'] | |||||
if remaining_threads: | |||||
sys.stderr.write( | |||||
'\n\n**WARNING**: Remaining threads at teardown: %r...\n' % ( | |||||
remaining_threads)) | |||||
def find_distribution_modules(name=__name__, file=__file__): | |||||
current_dist_depth = len(name.split('.')) - 1 | |||||
current_dist = os.path.join(os.path.dirname(file), | |||||
*([os.pardir] * current_dist_depth)) | |||||
abs = os.path.abspath(current_dist) | |||||
dist_name = os.path.basename(abs) | |||||
for dirpath, dirnames, filenames in os.walk(abs): | |||||
package = (dist_name + dirpath[len(abs):]).replace('/', '.') | |||||
if '__init__.py' in filenames: | |||||
yield package | |||||
for filename in filenames: | |||||
if filename.endswith('.py') and filename != '__init__.py': | |||||
yield '.'.join([package, filename])[:-3] | |||||
def import_all_modules(name=__name__, file=__file__, | |||||
skip=('celery.decorators', | |||||
'celery.contrib.batches', | |||||
'celery.task')): | |||||
for module in find_distribution_modules(name, file): | |||||
if not module.startswith(skip): | |||||
try: | |||||
import_module(module) | |||||
except ImportError: | |||||
pass |