@@ -1,5 +1 @@ | |||
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'] |
@@ -4,8 +4,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin | |||
from django.contrib.auth.models import User | |||
from .models import Post, CustomUser | |||
from .models import ScheduledReport, ReportRecipient, ScheduledReportGroup | |||
from .forms import ScheduledReportForm | |||
class CustomUserInline(admin.StackedInline): | |||
@@ -23,42 +22,3 @@ admin.site.register(User, UserAdmin) | |||
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) |
@@ -1,13 +0,0 @@ | |||
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) |
@@ -9,7 +9,6 @@ from django.contrib.auth.forms import UserCreationForm, UserChangeForm | |||
from datetime import datetime | |||
from croniter import croniter | |||
from django.forms import ModelForm, ValidationError | |||
from .models import ScheduledReport | |||
class PostForm(forms.ModelForm): | |||
class Meta: | |||
@@ -21,27 +20,3 @@ class NewTagForm(forms.ModelForm): | |||
class Meta: | |||
model = CustomUser | |||
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 |
@@ -0,0 +1,37 @@ | |||
# 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', | |||
), | |||
] |
@@ -26,40 +26,4 @@ class Post(models.Model): | |||
self.save() | |||
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 |
@@ -1,7 +0,0 @@ | |||
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() |
@@ -400,3 +400,7 @@ | |||
[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: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 |
@@ -13,7 +13,6 @@ https://docs.djangoproject.com/en/2.0/ref/settings/ | |||
import os | |||
import re | |||
import socket | |||
import djcelery | |||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) | |||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |||
@@ -47,7 +46,6 @@ INSTALLED_APPS = [ | |||
'application', | |||
'taggit', | |||
'taggit_templatetags2', | |||
'djcelery', | |||
'kombu.transport.django', | |||
] | |||
@@ -253,12 +251,3 @@ if DEBUG: | |||
DEBUG_TOOLBAR_CONFIG = { | |||
'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() |
@@ -1,155 +0,0 @@ | |||
# -*- 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, | |||
) |
@@ -1,54 +0,0 @@ | |||
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() |
@@ -1,159 +0,0 @@ | |||
# -*- 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 |
@@ -1,150 +0,0 @@ | |||
# -*- 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) |
@@ -1,512 +0,0 @@ | |||
# -*- 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) |
@@ -1,58 +0,0 @@ | |||
# -*- 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) |
@@ -1,675 +0,0 @@ | |||
# -*- 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 |
@@ -1,379 +0,0 @@ | |||
# -*- 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 |
@@ -1,317 +0,0 @@ | |||
# -*- 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, | |||
) |
@@ -1,274 +0,0 @@ | |||
# -*- 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()]) |
@@ -1,257 +0,0 @@ | |||
# -*- 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 |
@@ -1,71 +0,0 @@ | |||
# -*- 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] |
@@ -1,95 +0,0 @@ | |||
# -*- 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] |
@@ -1,948 +0,0 @@ | |||
# -*- 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 |
@@ -1,441 +0,0 @@ | |||
# -*- 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 |
@@ -1,266 +0,0 @@ | |||
# -*- 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 |
@@ -1,151 +0,0 @@ | |||
# -*- 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) |
@@ -1,372 +0,0 @@ | |||
# -*- 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 |
@@ -1,68 +0,0 @@ | |||
# -*- 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 |
@@ -1,317 +0,0 @@ | |||
# -*- 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) |
@@ -1,623 +0,0 @@ | |||
# -*- 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 |
@@ -1,161 +0,0 @@ | |||
# -*- 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) |
@@ -1,196 +0,0 @@ | |||
# -* 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) |
@@ -1,116 +0,0 @@ | |||
# -*- 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) |
@@ -1,201 +0,0 @@ | |||
# -*- 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) |
@@ -1,74 +0,0 @@ | |||
# -*- 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) |
@@ -1,62 +0,0 @@ | |||
# -*- 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() |
@@ -1,264 +0,0 @@ | |||
# -*- 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]) |
@@ -1,295 +0,0 @@ | |||
# -*- 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'] |
@@ -1,67 +0,0 @@ | |||
# -*- 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 |
@@ -1,571 +0,0 @@ | |||
# -*- 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) |
@@ -1,5 +0,0 @@ | |||
from __future__ import absolute_import | |||
from .base import Option | |||
__all__ = ['Option'] |
@@ -1,380 +0,0 @@ | |||
# -*- 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() |
@@ -1,668 +0,0 @@ | |||
# -*- 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), | |||
) |
@@ -1,100 +0,0 @@ | |||
# -*- 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() |
@@ -1,850 +0,0 @@ | |||
# -*- 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() |
@@ -1,181 +0,0 @@ | |||
# -*- 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() |
@@ -1,139 +0,0 @@ | |||
# -*- 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() |
@@ -1,191 +0,0 @@ | |||
# -*- 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) |
@@ -1,646 +0,0 @@ | |||
# -*- 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() |
@@ -1,270 +0,0 @@ | |||
# -*- 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() |
@@ -1,422 +0,0 @@ | |||
# -*- 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) |
@@ -1,698 +0,0 @@ | |||
# -*- 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 |
@@ -1,29 +0,0 @@ | |||
# -*- 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) |
@@ -1,171 +0,0 @@ | |||
# -*- 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 |
@@ -1,161 +0,0 @@ | |||
# -*- 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(), | |||
} |
@@ -1,136 +0,0 @@ | |||
# -*- 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) |
@@ -1,178 +0,0 @@ | |||
# -*- 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 |
@@ -1,30 +0,0 @@ | |||
# -*- 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': ()} |
@@ -1,57 +0,0 @@ | |||
# -*- 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 |
@@ -1,172 +0,0 @@ | |||
# -*- 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() |
@@ -1,249 +0,0 @@ | |||
# -*- 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, | |||
) |
@@ -1,126 +0,0 @@ | |||
# -*- 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)) |
@@ -1,365 +0,0 @@ | |||
# -*- 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) |
@@ -1,183 +0,0 @@ | |||
# -*- 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) |
@@ -1,76 +0,0 @@ | |||
# -*- 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) |
@@ -1,671 +0,0 @@ | |||
# -*- 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) |
@@ -1,408 +0,0 @@ | |||
# -*- 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 |
@@ -1,544 +0,0 @@ | |||
# -*- 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() |
@@ -1,109 +0,0 @@ | |||
# -*- 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() |
@@ -1,114 +0,0 @@ | |||
# -*- 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() |
@@ -1,656 +0,0 @@ | |||
# -*- 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, | |||
) |
@@ -1,171 +0,0 @@ | |||
# -*- 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.""" |
@@ -1,392 +0,0 @@ | |||
# -*- 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) |
@@ -1,266 +0,0 @@ | |||
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!') |
@@ -1,37 +0,0 @@ | |||
# -*- 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 |
@@ -1,17 +0,0 @@ | |||
# -*- 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 |
@@ -1,299 +0,0 @@ | |||
# -*- 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)) |
@@ -1,52 +0,0 @@ | |||
# -*- 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) |
@@ -1,373 +0,0 @@ | |||
# -*- 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 |
@@ -1,813 +0,0 @@ | |||
# -*- 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, | |||
))) |
@@ -1,925 +0,0 @@ | |||
# -*- 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 |
@@ -1,593 +0,0 @@ | |||
# -*- 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 |
@@ -1,71 +0,0 @@ | |||
# -*- 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) |
@@ -1,93 +0,0 @@ | |||
# -*- 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) |
@@ -1,27 +0,0 @@ | |||
# -*- 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) |
@@ -1,110 +0,0 @@ | |||
# -*- 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') |
@@ -1,35 +0,0 @@ | |||
# -*- 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]) |
@@ -1,76 +0,0 @@ | |||
# -*- 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']) |
@@ -1,153 +0,0 @@ | |||
# -*- 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]) |
@@ -1,59 +0,0 @@ | |||
# -*- 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'] | |||
), | |||
) |
@@ -1,179 +0,0 @@ | |||
# -*- 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)) |
@@ -1,220 +0,0 @@ | |||
# -*- 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) |
@@ -1,88 +0,0 @@ | |||
# -*- 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 |
@@ -1,12 +0,0 @@ | |||
"""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 |
@@ -1,87 +0,0 @@ | |||
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 |