added celery and tasks but most important added tag_list
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
"""Old django celery integration project."""
|
||||
# :copyright: (c) 2009 - 2015 by Ask Solem.
|
||||
# :license: BSD, see LICENSE for more details.
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
VERSION = (3, 2, 2)
|
||||
__version__ = '.'.join(map(str, VERSION[0:3])) + ''.join(VERSION[3:])
|
||||
__author__ = 'Ask Solem'
|
||||
__contact__ = 'ask@celeryproject.org'
|
||||
__homepage__ = 'http://celeryproject.org'
|
||||
__docformat__ = 'restructuredtext'
|
||||
__license__ = 'BSD (3 clause)'
|
||||
|
||||
# -eof meta-
|
||||
|
||||
|
||||
if sys.version_info[0] == 3:
|
||||
|
||||
def setup_loader():
|
||||
os.environ.setdefault(
|
||||
'CELERY_LOADER', 'djcelery.loaders.DjangoLoader',
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
def setup_loader(): # noqa
|
||||
os.environ.setdefault(
|
||||
b'CELERY_LOADER', b'djcelery.loaders.DjangoLoader',
|
||||
)
|
||||
|
||||
from celery import current_app as celery # noqa
|
||||
@@ -0,0 +1,385 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from anyjson import loads
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import helpers
|
||||
from django.contrib.admin.views import main as main_views
|
||||
from django.forms.widgets import Select
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from celery import current_app
|
||||
from celery import states
|
||||
from celery.task.control import broadcast, revoke, rate_limit
|
||||
from celery.utils import cached_property
|
||||
from celery.utils.text import abbrtask
|
||||
|
||||
from .admin_utils import action, display_field, fixedwidth
|
||||
from .models import (
|
||||
TaskState, WorkerState,
|
||||
PeriodicTask, IntervalSchedule, CrontabSchedule,
|
||||
PeriodicTasks
|
||||
)
|
||||
from .humanize import naturaldate
|
||||
from .utils import is_database_scheduler, make_aware
|
||||
|
||||
try:
|
||||
from django.utils.encoding import force_text
|
||||
except ImportError:
|
||||
from django.utils.encoding import force_unicode as force_text # noqa
|
||||
|
||||
|
||||
TASK_STATE_COLORS = {states.SUCCESS: 'green',
|
||||
states.FAILURE: 'red',
|
||||
states.REVOKED: 'magenta',
|
||||
states.STARTED: 'yellow',
|
||||
states.RETRY: 'orange',
|
||||
'RECEIVED': 'blue'}
|
||||
NODE_STATE_COLORS = {'ONLINE': 'green',
|
||||
'OFFLINE': 'gray'}
|
||||
|
||||
|
||||
class MonitorList(main_views.ChangeList):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MonitorList, self).__init__(*args, **kwargs)
|
||||
self.title = self.model_admin.list_page_title
|
||||
|
||||
|
||||
@display_field(_('state'), 'state')
|
||||
def colored_state(task):
|
||||
state = escape(task.state)
|
||||
color = TASK_STATE_COLORS.get(task.state, 'black')
|
||||
return '<b><span style="color: {0};">{1}</span></b>'.format(color, state)
|
||||
|
||||
|
||||
@display_field(_('state'), 'last_heartbeat')
|
||||
def node_state(node):
|
||||
state = node.is_alive() and 'ONLINE' or 'OFFLINE'
|
||||
color = NODE_STATE_COLORS[state]
|
||||
return '<b><span style="color: {0};">{1}</span></b>'.format(color, state)
|
||||
|
||||
|
||||
@display_field(_('ETA'), 'eta')
|
||||
def eta(task):
|
||||
if not task.eta:
|
||||
return '<span style="color: gray;">none</span>'
|
||||
return escape(make_aware(task.eta))
|
||||
|
||||
|
||||
@display_field(_('when'), 'tstamp')
|
||||
def tstamp(task):
|
||||
# convert to local timezone
|
||||
value = make_aware(task.tstamp)
|
||||
return '<div title="{0}">{1}</div>'.format(
|
||||
escape(str(value)), escape(naturaldate(value)),
|
||||
)
|
||||
|
||||
|
||||
@display_field(_('name'), 'name')
|
||||
def name(task):
|
||||
short_name = abbrtask(task.name, 16)
|
||||
return '<div title="{0}"><b>{1}</b></div>'.format(
|
||||
escape(task.name), escape(short_name),
|
||||
)
|
||||
|
||||
|
||||
class ModelMonitor(admin.ModelAdmin):
|
||||
can_add = False
|
||||
can_delete = False
|
||||
|
||||
def get_changelist(self, request, **kwargs):
|
||||
return MonitorList
|
||||
|
||||
def change_view(self, request, object_id, extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context.setdefault('title', self.detail_title)
|
||||
return super(ModelMonitor, self).change_view(
|
||||
request, object_id, extra_context=extra_context,
|
||||
)
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
if not self.can_delete:
|
||||
return False
|
||||
return super(ModelMonitor, self).has_delete_permission(request, obj)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
if not self.can_add:
|
||||
return False
|
||||
return super(ModelMonitor, self).has_add_permission(request)
|
||||
|
||||
|
||||
class TaskMonitor(ModelMonitor):
|
||||
detail_title = _('Task detail')
|
||||
list_page_title = _('Tasks')
|
||||
rate_limit_confirmation_template = 'djcelery/confirm_rate_limit.html'
|
||||
date_hierarchy = 'tstamp'
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('state', 'task_id', 'name', 'args', 'kwargs',
|
||||
'eta', 'runtime', 'worker', 'tstamp'),
|
||||
'classes': ('extrapretty', ),
|
||||
}),
|
||||
('Details', {
|
||||
'classes': ('collapse', 'extrapretty'),
|
||||
'fields': ('result', 'traceback', 'expires'),
|
||||
}),
|
||||
)
|
||||
list_display = (
|
||||
fixedwidth('task_id', name=_('UUID'), pt=8),
|
||||
colored_state,
|
||||
name,
|
||||
fixedwidth('args', pretty=True),
|
||||
fixedwidth('kwargs', pretty=True),
|
||||
eta,
|
||||
tstamp,
|
||||
'worker',
|
||||
)
|
||||
readonly_fields = (
|
||||
'state', 'task_id', 'name', 'args', 'kwargs',
|
||||
'eta', 'runtime', 'worker', 'result', 'traceback',
|
||||
'expires', 'tstamp',
|
||||
)
|
||||
list_filter = ('state', 'name', 'tstamp', 'eta', 'worker')
|
||||
search_fields = ('name', 'task_id', 'args', 'kwargs', 'worker__hostname')
|
||||
actions = ['revoke_tasks',
|
||||
'terminate_tasks',
|
||||
'kill_tasks',
|
||||
'rate_limit_tasks']
|
||||
|
||||
class Media:
|
||||
css = {'all': ('djcelery/style.css', )}
|
||||
|
||||
@action(_('Revoke selected tasks'))
|
||||
def revoke_tasks(self, request, queryset):
|
||||
with current_app.default_connection() as connection:
|
||||
for state in queryset:
|
||||
revoke(state.task_id, connection=connection)
|
||||
|
||||
@action(_('Terminate selected tasks'))
|
||||
def terminate_tasks(self, request, queryset):
|
||||
with current_app.default_connection() as connection:
|
||||
for state in queryset:
|
||||
revoke(state.task_id, connection=connection, terminate=True)
|
||||
|
||||
@action(_('Kill selected tasks'))
|
||||
def kill_tasks(self, request, queryset):
|
||||
with current_app.default_connection() as connection:
|
||||
for state in queryset:
|
||||
revoke(state.task_id, connection=connection,
|
||||
terminate=True, signal='KILL')
|
||||
|
||||
@action(_('Rate limit selected tasks'))
|
||||
def rate_limit_tasks(self, request, queryset):
|
||||
tasks = set([task.name for task in queryset])
|
||||
opts = self.model._meta
|
||||
app_label = opts.app_label
|
||||
if request.POST.get('post'):
|
||||
rate = request.POST['rate_limit']
|
||||
with current_app.default_connection() as connection:
|
||||
for task_name in tasks:
|
||||
rate_limit(task_name, rate, connection=connection)
|
||||
return None
|
||||
|
||||
context = {
|
||||
'title': _('Rate limit selection'),
|
||||
'queryset': queryset,
|
||||
'object_name': force_text(opts.verbose_name),
|
||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||
'opts': opts,
|
||||
'app_label': app_label,
|
||||
}
|
||||
|
||||
return render_to_response(
|
||||
self.rate_limit_confirmation_template, context,
|
||||
context_instance=RequestContext(request),
|
||||
)
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super(TaskMonitor, self).get_actions(request)
|
||||
actions.pop('delete_selected', None)
|
||||
return actions
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(TaskMonitor, self).get_queryset(request)
|
||||
return qs.select_related('worker')
|
||||
|
||||
|
||||
class WorkerMonitor(ModelMonitor):
|
||||
can_add = True
|
||||
detail_title = _('Node detail')
|
||||
list_page_title = _('Worker Nodes')
|
||||
list_display = ('hostname', node_state)
|
||||
readonly_fields = ('last_heartbeat', )
|
||||
actions = ['shutdown_nodes',
|
||||
'enable_events',
|
||||
'disable_events']
|
||||
|
||||
@action(_('Shutdown selected worker nodes'))
|
||||
def shutdown_nodes(self, request, queryset):
|
||||
broadcast('shutdown', destination=[n.hostname for n in queryset])
|
||||
|
||||
@action(_('Enable event mode for selected nodes.'))
|
||||
def enable_events(self, request, queryset):
|
||||
broadcast('enable_events',
|
||||
destination=[n.hostname for n in queryset])
|
||||
|
||||
@action(_('Disable event mode for selected nodes.'))
|
||||
def disable_events(self, request, queryset):
|
||||
broadcast('disable_events',
|
||||
destination=[n.hostname for n in queryset])
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super(WorkerMonitor, self).get_actions(request)
|
||||
actions.pop('delete_selected', None)
|
||||
return actions
|
||||
|
||||
|
||||
admin.site.register(TaskState, TaskMonitor)
|
||||
admin.site.register(WorkerState, WorkerMonitor)
|
||||
|
||||
|
||||
# ### Periodic Tasks
|
||||
|
||||
|
||||
class TaskSelectWidget(Select):
|
||||
celery_app = current_app
|
||||
_choices = None
|
||||
|
||||
def tasks_as_choices(self):
|
||||
_ = self._modules # noqa
|
||||
tasks = list(sorted(name for name in self.celery_app.tasks
|
||||
if not name.startswith('celery.')))
|
||||
return (('', ''), ) + tuple(zip(tasks, tasks))
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
if self._choices is None:
|
||||
self._choices = self.tasks_as_choices()
|
||||
return self._choices
|
||||
|
||||
@choices.setter
|
||||
def choices(self, _):
|
||||
# ChoiceField.__init__ sets ``self.choices = choices``
|
||||
# which would override ours.
|
||||
pass
|
||||
|
||||
@cached_property
|
||||
def _modules(self):
|
||||
self.celery_app.loader.import_default_modules()
|
||||
|
||||
|
||||
class TaskChoiceField(forms.ChoiceField):
|
||||
widget = TaskSelectWidget
|
||||
|
||||
def valid_value(self, value):
|
||||
return True
|
||||
|
||||
|
||||
class PeriodicTaskForm(forms.ModelForm):
|
||||
regtask = TaskChoiceField(label=_('Task (registered)'),
|
||||
required=False)
|
||||
task = forms.CharField(label=_('Task (custom)'), required=False,
|
||||
max_length=200)
|
||||
|
||||
class Meta:
|
||||
model = PeriodicTask
|
||||
exclude = ()
|
||||
|
||||
def clean(self):
|
||||
data = super(PeriodicTaskForm, self).clean()
|
||||
regtask = data.get('regtask')
|
||||
if regtask:
|
||||
data['task'] = regtask
|
||||
if not data['task']:
|
||||
exc = forms.ValidationError(_('Need name of task'))
|
||||
self._errors['task'] = self.error_class(exc.messages)
|
||||
raise exc
|
||||
return data
|
||||
|
||||
def _clean_json(self, field):
|
||||
value = self.cleaned_data[field]
|
||||
try:
|
||||
loads(value)
|
||||
except ValueError as exc:
|
||||
raise forms.ValidationError(
|
||||
_('Unable to parse JSON: %s') % exc,
|
||||
)
|
||||
return value
|
||||
|
||||
def clean_args(self):
|
||||
return self._clean_json('args')
|
||||
|
||||
def clean_kwargs(self):
|
||||
return self._clean_json('kwargs')
|
||||
|
||||
|
||||
class PeriodicTaskAdmin(admin.ModelAdmin):
|
||||
form = PeriodicTaskForm
|
||||
model = PeriodicTask
|
||||
list_display = (
|
||||
'enabled',
|
||||
'__unicode__',
|
||||
'task',
|
||||
'args',
|
||||
'kwargs',
|
||||
)
|
||||
search_fields = ('name', 'task')
|
||||
list_display_links = ('enabled', '__unicode__', 'task')
|
||||
ordering = ('-enabled', 'name')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('name', 'regtask', 'task', 'enabled'),
|
||||
'classes': ('extrapretty', 'wide'),
|
||||
}),
|
||||
('Schedule', {
|
||||
'fields': ('interval', 'crontab'),
|
||||
'classes': ('extrapretty', 'wide', ),
|
||||
}),
|
||||
('Arguments', {
|
||||
'fields': ('args', 'kwargs'),
|
||||
'classes': ('extrapretty', 'wide', 'collapse'),
|
||||
}),
|
||||
('Execution Options', {
|
||||
'fields': ('expires', 'queue', 'exchange', 'routing_key'),
|
||||
'classes': ('extrapretty', 'wide', 'collapse'),
|
||||
}),
|
||||
)
|
||||
actions = ['enable_tasks',
|
||||
'disable_tasks']
|
||||
|
||||
def update_periodic_tasks(self):
|
||||
dummy_periodic_task = PeriodicTask()
|
||||
dummy_periodic_task.no_changes = False
|
||||
PeriodicTasks.changed(dummy_periodic_task)
|
||||
|
||||
@action(_('Enable selected periodic tasks'))
|
||||
def enable_tasks(self, request, queryset):
|
||||
queryset.update(enabled=True)
|
||||
self.update_periodic_tasks()
|
||||
|
||||
@action(_('Disable selected periodic tasks'))
|
||||
def disable_tasks(self, request, queryset):
|
||||
queryset.update(enabled=False)
|
||||
self.update_periodic_tasks()
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
scheduler = getattr(settings, 'CELERYBEAT_SCHEDULER', None)
|
||||
extra_context['wrong_scheduler'] = not is_database_scheduler(scheduler)
|
||||
return super(PeriodicTaskAdmin, self).changelist_view(request,
|
||||
extra_context)
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(PeriodicTaskAdmin, self).get_queryset(request)
|
||||
return qs.select_related('interval', 'crontab')
|
||||
|
||||
|
||||
admin.site.register(IntervalSchedule)
|
||||
admin.site.register(CrontabSchedule)
|
||||
admin.site.register(PeriodicTask, PeriodicTaskAdmin)
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from pprint import pformat
|
||||
|
||||
from django.utils.html import escape
|
||||
|
||||
FIXEDWIDTH_STYLE = '''\
|
||||
<span title="{0}" style="font-size: {1}pt; \
|
||||
font-family: Menlo, Courier; ">{2}</span> \
|
||||
'''
|
||||
|
||||
|
||||
def attrs(**kwargs):
|
||||
def _inner(fun):
|
||||
for attr_name, attr_value in kwargs.items():
|
||||
setattr(fun, attr_name, attr_value)
|
||||
return fun
|
||||
return _inner
|
||||
|
||||
|
||||
def display_field(short_description, admin_order_field,
|
||||
allow_tags=True, **kwargs):
|
||||
return attrs(short_description=short_description,
|
||||
admin_order_field=admin_order_field,
|
||||
allow_tags=allow_tags, **kwargs)
|
||||
|
||||
|
||||
def action(short_description, **kwargs):
|
||||
return attrs(short_description=short_description, **kwargs)
|
||||
|
||||
|
||||
def fixedwidth(field, name=None, pt=6, width=16, maxlen=64, pretty=False):
|
||||
|
||||
@display_field(name or field, field)
|
||||
def f(task):
|
||||
val = getattr(task, field)
|
||||
if pretty:
|
||||
val = pformat(val, width=width)
|
||||
if val.startswith("u'") or val.startswith('u"'):
|
||||
val = val[2:-1]
|
||||
shortval = val.replace(',', ',\n')
|
||||
shortval = shortval.replace('\n', '|br/|')
|
||||
|
||||
if len(shortval) > maxlen:
|
||||
shortval = shortval[:maxlen] + '...'
|
||||
styled = FIXEDWIDTH_STYLE.format(
|
||||
escape(val[:255]), pt, escape(shortval),
|
||||
)
|
||||
return styled.replace('|br/|', '<br/>')
|
||||
return f
|
||||
@@ -0,0 +1,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery import current_app
|
||||
|
||||
|
||||
#: The Django-Celery app instance.
|
||||
app = current_app._get_current_object()
|
||||
@@ -0,0 +1,34 @@
|
||||
"""celery.backends.cache"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.cache import cache, caches
|
||||
|
||||
from celery import current_app
|
||||
from celery.backends.base import KeyValueStoreBackend
|
||||
|
||||
# CELERY_CACHE_BACKEND overrides the django-global(tm) backend settings.
|
||||
if current_app.conf.CELERY_CACHE_BACKEND:
|
||||
cache = caches[current_app.conf.CELERY_CACHE_BACKEND] # noqa
|
||||
|
||||
|
||||
class CacheBackend(KeyValueStoreBackend):
|
||||
"""Backend using the Django cache framework to store task metadata."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CacheBackend, self).__init__(*args, **kwargs)
|
||||
expires = kwargs.get('expires',
|
||||
current_app.conf.CELERY_TASK_RESULT_EXPIRES)
|
||||
if isinstance(expires, timedelta):
|
||||
expires = int(max(expires.total_seconds(), 0))
|
||||
self.expires = expires
|
||||
|
||||
def get(self, key):
|
||||
return cache.get(key)
|
||||
|
||||
def set(self, key, value):
|
||||
cache.set(key, value, self.expires)
|
||||
|
||||
def delete(self, key):
|
||||
cache.delete(key)
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery import current_app
|
||||
from celery.backends.base import BaseDictBackend
|
||||
|
||||
try:
|
||||
from celery.utils.timeutils import maybe_timedelta
|
||||
except ImportError:
|
||||
from celery.utils.time import maybe_timedelta
|
||||
|
||||
from ..models import TaskMeta, TaskSetMeta
|
||||
|
||||
|
||||
class DatabaseBackend(BaseDictBackend):
|
||||
"""The database backend.
|
||||
|
||||
Using Django models to store task state.
|
||||
|
||||
"""
|
||||
TaskModel = TaskMeta
|
||||
TaskSetModel = TaskSetMeta
|
||||
|
||||
expires = current_app.conf.CELERY_TASK_RESULT_EXPIRES
|
||||
create_django_tables = True
|
||||
|
||||
subpolling_interval = 0.5
|
||||
|
||||
def _store_result(self, task_id, result, status,
|
||||
traceback=None, request=None):
|
||||
"""Store return value and status of an executed task."""
|
||||
self.TaskModel._default_manager.store_result(
|
||||
task_id, result, status,
|
||||
traceback=traceback, children=self.current_task_children(request),
|
||||
)
|
||||
return result
|
||||
|
||||
def _save_group(self, group_id, result):
|
||||
"""Store the result of an executed group."""
|
||||
self.TaskSetModel._default_manager.store_result(group_id, result)
|
||||
return result
|
||||
|
||||
def _get_task_meta_for(self, task_id):
|
||||
"""Get task metadata for a task by id."""
|
||||
return self.TaskModel._default_manager.get_task(task_id).to_dict()
|
||||
|
||||
def _restore_group(self, group_id):
|
||||
"""Get group metadata for a group by id."""
|
||||
meta = self.TaskSetModel._default_manager.restore_taskset(group_id)
|
||||
if meta:
|
||||
return meta.to_dict()
|
||||
|
||||
def _delete_group(self, group_id):
|
||||
self.TaskSetModel._default_manager.delete_taskset(group_id)
|
||||
|
||||
def _forget(self, task_id):
|
||||
try:
|
||||
self.TaskModel._default_manager.get(task_id=task_id).delete()
|
||||
except self.TaskModel.DoesNotExist:
|
||||
pass
|
||||
|
||||
def cleanup(self):
|
||||
"""Delete expired metadata."""
|
||||
expires = maybe_timedelta(self.expires)
|
||||
for model in self.TaskModel, self.TaskSetModel:
|
||||
model._default_manager.delete_expired(expires)
|
||||
@@ -0,0 +1,72 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
|
||||
from django.utils import translation
|
||||
|
||||
|
||||
@contextmanager
|
||||
def respect_language(language):
|
||||
"""Context manager that changes the current translation language for
|
||||
all code inside the following block.
|
||||
|
||||
Can e.g. be used inside tasks like this::
|
||||
|
||||
from celery import task
|
||||
from djcelery.common import respect_language
|
||||
|
||||
@task
|
||||
def my_task(language=None):
|
||||
with respect_language(language):
|
||||
pass
|
||||
"""
|
||||
if language:
|
||||
prev = translation.get_language()
|
||||
translation.activate(language)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
translation.activate(prev)
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
def respects_language(fun):
|
||||
"""Decorator for tasks with respect to site's current language.
|
||||
You can use this decorator on your tasks together with default @task
|
||||
decorator (remember that the task decorator must be applied last).
|
||||
|
||||
See also the with-statement alternative :func:`respect_language`.
|
||||
|
||||
**Example**:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@task
|
||||
@respects_language
|
||||
def my_task()
|
||||
# localize something.
|
||||
|
||||
The task will then accept a ``language`` argument that will be
|
||||
used to set the language in the task, and the task can thus be
|
||||
called like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.utils import translation
|
||||
from myapp.tasks import my_task
|
||||
|
||||
# Pass the current language on to the task
|
||||
my_task.delay(language=translation.get_language())
|
||||
|
||||
# or set the language explicitly
|
||||
my_task.delay(language='no.no')
|
||||
|
||||
"""
|
||||
|
||||
@wraps(fun)
|
||||
def _inner(*args, **kwargs):
|
||||
with respect_language(kwargs.pop('language', None)):
|
||||
return fun(*args, **kwargs)
|
||||
return _inner
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from kombu.utils.encoding import bytes_to_str, str_to_bytes
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
|
||||
def python_2_unicode_compatible(cls):
|
||||
"""Taken from Django project (django/utils/encoding.py) & modified a bit to
|
||||
always have __unicode__ method available.
|
||||
"""
|
||||
if '__str__' not in cls.__dict__:
|
||||
raise ValueError("@python_2_unicode_compatible cannot be applied "
|
||||
"to %s because it doesn't define __str__()." %
|
||||
cls.__name__)
|
||||
|
||||
cls.__unicode__ = cls.__str__
|
||||
|
||||
if PY2:
|
||||
cls.__str__ = lambda self: self.__unicode__().encode('utf-8')
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
if PY3:
|
||||
unicode = str
|
||||
|
||||
def itervalues(x):
|
||||
return x.values()
|
||||
|
||||
def setenv(k, v):
|
||||
os.environ[bytes_to_str(k)] = bytes_to_str(v)
|
||||
else:
|
||||
unicode = unicode
|
||||
|
||||
def itervalues(x): # noqa
|
||||
return x.itervalues()
|
||||
|
||||
def setenv(k, v): # noqa
|
||||
os.environ[str_to_bytes(k)] = str_to_bytes(v)
|
||||
@@ -0,0 +1,69 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
try:
|
||||
from django.test.runner import DiscoverRunner
|
||||
except ImportError:
|
||||
from django.test.simple import DjangoTestSuiteRunner as DiscoverRunner
|
||||
|
||||
from celery import current_app
|
||||
from celery.task import Task
|
||||
from djcelery.backends.database import DatabaseBackend
|
||||
|
||||
|
||||
USAGE = """\
|
||||
Custom test runner to allow testing of celery delayed tasks.
|
||||
"""
|
||||
|
||||
|
||||
def _set_eager():
|
||||
settings.CELERY_ALWAYS_EAGER = True
|
||||
current_app.conf.CELERY_ALWAYS_EAGER = True
|
||||
settings.CELERY_EAGER_PROPAGATES_EXCEPTIONS = True # Issue #75
|
||||
current_app.conf.CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
|
||||
|
||||
|
||||
class CeleryTestSuiteRunner(DiscoverRunner):
|
||||
"""Django test runner allowing testing of celery delayed tasks.
|
||||
|
||||
All tasks are run locally, not in a worker.
|
||||
|
||||
To use this runner set ``settings.TEST_RUNNER``::
|
||||
|
||||
TEST_RUNNER = 'djcelery.contrib.test_runner.CeleryTestSuiteRunner'
|
||||
|
||||
"""
|
||||
def setup_test_environment(self, **kwargs):
|
||||
_set_eager()
|
||||
super(CeleryTestSuiteRunner, self).setup_test_environment(**kwargs)
|
||||
|
||||
|
||||
class CeleryTestSuiteRunnerStoringResult(DiscoverRunner):
|
||||
"""Django test runner allowing testing of celery delayed tasks,
|
||||
and storing the results of those tasks in ``TaskMeta``.
|
||||
|
||||
Requires setting CELERY_RESULT_BACKEND = 'database'.
|
||||
|
||||
USAGE:
|
||||
|
||||
In ``settings.py``::
|
||||
|
||||
TEST_RUNNER = '''
|
||||
djcelery.contrib.test_runner.CeleryTestSuiteRunnerStoringResult
|
||||
'''.strip()
|
||||
|
||||
"""
|
||||
|
||||
def setup_test_environment(self, **kwargs):
|
||||
# Monkey-patch Task.on_success() method
|
||||
def on_success_patched(self, retval, task_id, args, kwargs):
|
||||
app = current_app._get_current_object()
|
||||
DatabaseBackend(app=app).store_result(task_id, retval, 'SUCCESS')
|
||||
Task.on_success = classmethod(on_success_patched)
|
||||
|
||||
super(CeleryTestSuiteRunnerStoringResult, self).setup_test_environment(
|
||||
**kwargs
|
||||
)
|
||||
|
||||
settings.CELERY_RESULT_BACKEND = 'database'
|
||||
_set_eager()
|
||||
@@ -0,0 +1,63 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import django
|
||||
|
||||
from contextlib import contextmanager
|
||||
from django.db import transaction
|
||||
|
||||
if django.VERSION < (1, 6): # pragma: no cover
|
||||
|
||||
def get_queryset(s):
|
||||
return s.get_query_set()
|
||||
else:
|
||||
def get_queryset(s): # noqa
|
||||
return s.get_queryset()
|
||||
|
||||
try:
|
||||
from django.db.transaction import atomic # noqa
|
||||
except ImportError: # pragma: no cover
|
||||
|
||||
try:
|
||||
from django.db.transaction import Transaction # noqa
|
||||
except ImportError:
|
||||
@contextmanager
|
||||
def commit_on_success(*args, **kwargs):
|
||||
try:
|
||||
transaction.enter_transaction_management(*args, **kwargs)
|
||||
transaction.managed(True, *args, **kwargs)
|
||||
try:
|
||||
yield
|
||||
except:
|
||||
if transaction.is_dirty(*args, **kwargs):
|
||||
transaction.rollback(*args, **kwargs)
|
||||
raise
|
||||
else:
|
||||
if transaction.is_dirty(*args, **kwargs):
|
||||
try:
|
||||
transaction.commit(*args, **kwargs)
|
||||
except:
|
||||
transaction.rollback(*args, **kwargs)
|
||||
raise
|
||||
finally:
|
||||
transaction.leave_transaction_management(*args, **kwargs)
|
||||
else: # pragma: no cover
|
||||
from django.db.transaction import commit_on_success # noqa
|
||||
|
||||
commit_unless_managed = transaction.commit_unless_managed
|
||||
rollback_unless_managed = transaction.rollback_unless_managed
|
||||
else:
|
||||
@contextmanager
|
||||
def commit_on_success(using=None): # noqa
|
||||
connection = transaction.get_connection(using)
|
||||
if connection.features.autocommits_when_autocommit_is_off:
|
||||
# ignore stupid warnings and errors
|
||||
yield
|
||||
else:
|
||||
with transaction.atomic(using):
|
||||
yield
|
||||
|
||||
def commit_unless_managed(*args, **kwargs): # noqa
|
||||
pass
|
||||
|
||||
def rollback_unless_managed(*args, **kwargs): # noqa
|
||||
pass
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils.translation import ungettext, ugettext as _
|
||||
from .utils import now
|
||||
|
||||
|
||||
def pluralize_year(n):
|
||||
return ungettext(_('{num} year ago'), _('{num} years ago'), n)
|
||||
|
||||
|
||||
def pluralize_month(n):
|
||||
return ungettext(_('{num} month ago'), _('{num} months ago'), n)
|
||||
|
||||
|
||||
def pluralize_week(n):
|
||||
return ungettext(_('{num} week ago'), _('{num} weeks ago'), n)
|
||||
|
||||
|
||||
def pluralize_day(n):
|
||||
return ungettext(_('{num} day ago'), _('{num} days ago'), n)
|
||||
|
||||
|
||||
OLDER_CHUNKS = (
|
||||
(365.0, pluralize_year),
|
||||
(30.0, pluralize_month),
|
||||
(7.0, pluralize_week),
|
||||
(1.0, pluralize_day),
|
||||
)
|
||||
|
||||
|
||||
def _un(singular__plural, n=None):
|
||||
singular, plural = singular__plural
|
||||
return ungettext(singular, plural, n)
|
||||
|
||||
|
||||
def naturaldate(date, include_seconds=False):
|
||||
"""Convert datetime into a human natural date string."""
|
||||
|
||||
if not date:
|
||||
return ''
|
||||
|
||||
right_now = now()
|
||||
today = datetime(right_now.year, right_now.month,
|
||||
right_now.day, tzinfo=right_now.tzinfo)
|
||||
delta = right_now - date
|
||||
delta_midnight = today - date
|
||||
|
||||
days = delta.days
|
||||
hours = delta.seconds // 3600
|
||||
minutes = delta.seconds // 60
|
||||
seconds = delta.seconds
|
||||
|
||||
if days < 0:
|
||||
return _('just now')
|
||||
|
||||
if days == 0:
|
||||
if hours == 0:
|
||||
if minutes > 0:
|
||||
return ungettext(
|
||||
_('{minutes} minute ago'),
|
||||
_('{minutes} minutes ago'), minutes
|
||||
).format(minutes=minutes)
|
||||
else:
|
||||
if include_seconds and seconds:
|
||||
return ungettext(
|
||||
_('{seconds} second ago'),
|
||||
_('{seconds} seconds ago'), seconds
|
||||
).format(seconds=seconds)
|
||||
return _('just now')
|
||||
else:
|
||||
return ungettext(
|
||||
_('{hours} hour ago'), _('{hours} hours ago'), hours
|
||||
).format(hours=hours)
|
||||
|
||||
if delta_midnight.days == 0:
|
||||
return _('yesterday at {time}').format(time=date.strftime('%H:%M'))
|
||||
|
||||
count = 0
|
||||
for chunk, pluralizefun in OLDER_CHUNKS:
|
||||
if days >= chunk:
|
||||
count = int(round((delta_midnight.days + 1) / chunk, 0))
|
||||
fmt = pluralizefun(count)
|
||||
return fmt.format(num=count)
|
||||
@@ -0,0 +1,202 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import imp
|
||||
import importlib
|
||||
|
||||
from datetime import datetime
|
||||
from warnings import warn
|
||||
|
||||
from celery import signals
|
||||
try:
|
||||
from celery.utils.collections import DictAttribute
|
||||
except ImportError:
|
||||
from celery.datastructures import DictAttribute
|
||||
from celery.loaders.base import BaseLoader
|
||||
|
||||
from django import db
|
||||
from django.conf import settings
|
||||
from django.core import cache
|
||||
from django.core.mail import mail_admins
|
||||
|
||||
from .utils import DATABASE_ERRORS, now
|
||||
|
||||
_RACE_PROTECTION = False
|
||||
|
||||
|
||||
def _maybe_close_fd(fh):
|
||||
try:
|
||||
os.close(fh.fileno())
|
||||
except (AttributeError, OSError, TypeError):
|
||||
# TypeError added for celery#962
|
||||
pass
|
||||
|
||||
|
||||
class DjangoLoader(BaseLoader):
|
||||
"""The Django loader."""
|
||||
_db_reuse = 0
|
||||
|
||||
override_backends = {
|
||||
'database': 'djcelery.backends.database.DatabaseBackend',
|
||||
'cache': 'djcelery.backends.cache.CacheBackend',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DjangoLoader, self).__init__(*args, **kwargs)
|
||||
self._install_signal_handlers()
|
||||
|
||||
def _install_signal_handlers(self):
|
||||
# Need to close any open database connection after
|
||||
# any embedded celerybeat process forks.
|
||||
signals.beat_embedded_init.connect(self.close_database)
|
||||
signals.worker_ready.connect(self.warn_if_debug)
|
||||
|
||||
def now(self, utc=False):
|
||||
return datetime.utcnow() if utc else now()
|
||||
|
||||
def read_configuration(self):
|
||||
"""Load configuration from Django settings."""
|
||||
self.configured = True
|
||||
# Default backend needs to be the database backend for backward
|
||||
# compatibility.
|
||||
backend = (getattr(settings, 'CELERY_RESULT_BACKEND', None) or
|
||||
getattr(settings, 'CELERY_BACKEND', None))
|
||||
if not backend:
|
||||
settings.CELERY_RESULT_BACKEND = 'database'
|
||||
return DictAttribute(settings)
|
||||
|
||||
def _close_database(self):
|
||||
try:
|
||||
funs = [conn.close for conn in db.connections]
|
||||
except AttributeError:
|
||||
if hasattr(db, 'close_old_connections'): # Django 1.6+
|
||||
funs = [db.close_old_connections]
|
||||
else:
|
||||
funs = [db.close_connection] # pre multidb
|
||||
|
||||
for close in funs:
|
||||
try:
|
||||
close()
|
||||
except DATABASE_ERRORS as exc:
|
||||
str_exc = str(exc)
|
||||
if 'closed' not in str_exc and 'not connected' not in str_exc:
|
||||
raise
|
||||
|
||||
def close_database(self, **kwargs):
|
||||
db_reuse_max = self.conf.get('CELERY_DB_REUSE_MAX', None)
|
||||
if not db_reuse_max:
|
||||
return self._close_database()
|
||||
if self._db_reuse >= db_reuse_max * 2:
|
||||
self._db_reuse = 0
|
||||
self._close_database()
|
||||
self._db_reuse += 1
|
||||
|
||||
def close_cache(self):
|
||||
try:
|
||||
cache.cache.close()
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
def on_process_cleanup(self):
|
||||
"""Does everything necessary for Django to work in a long-living,
|
||||
multiprocessing environment.
|
||||
|
||||
"""
|
||||
# See http://groups.google.com/group/django-users/
|
||||
# browse_thread/thread/78200863d0c07c6d/
|
||||
self.close_database()
|
||||
self.close_cache()
|
||||
|
||||
def on_task_init(self, task_id, task):
|
||||
"""Called before every task."""
|
||||
try:
|
||||
is_eager = task.request.is_eager
|
||||
except AttributeError:
|
||||
is_eager = False
|
||||
if not is_eager:
|
||||
self.close_database()
|
||||
|
||||
def on_worker_init(self):
|
||||
"""Called when the worker starts.
|
||||
|
||||
Automatically discovers any ``tasks.py`` files in the applications
|
||||
listed in ``INSTALLED_APPS``.
|
||||
|
||||
"""
|
||||
self.import_default_modules()
|
||||
|
||||
self.close_database()
|
||||
self.close_cache()
|
||||
|
||||
def warn_if_debug(self, **kwargs):
|
||||
if settings.DEBUG:
|
||||
warn('Using settings.DEBUG leads to a memory leak, never '
|
||||
'use this setting in production environments!')
|
||||
|
||||
def import_default_modules(self):
|
||||
super(DjangoLoader, self).import_default_modules()
|
||||
self.autodiscover()
|
||||
|
||||
def autodiscover(self):
|
||||
self.task_modules.update(mod.__name__ for mod in autodiscover() or ())
|
||||
|
||||
def on_worker_process_init(self):
|
||||
# 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 db.connections.all():
|
||||
if c and c.connection:
|
||||
_maybe_close_fd(c.connection)
|
||||
except AttributeError:
|
||||
if db.connection and db.connection.connection:
|
||||
_maybe_close_fd(db.connection.connection)
|
||||
|
||||
# use the _ version to avoid DB_REUSE preventing the conn.close() call
|
||||
self._close_database()
|
||||
self.close_cache()
|
||||
|
||||
def mail_admins(self, subject, body, fail_silently=False, **kwargs):
|
||||
return mail_admins(subject, body, fail_silently=fail_silently)
|
||||
|
||||
|
||||
def autodiscover():
|
||||
"""Include tasks for all applications in ``INSTALLED_APPS``."""
|
||||
global _RACE_PROTECTION
|
||||
|
||||
if _RACE_PROTECTION:
|
||||
return
|
||||
_RACE_PROTECTION = True
|
||||
try:
|
||||
return filter(None, [find_related_module(app, 'tasks')
|
||||
for app in settings.INSTALLED_APPS])
|
||||
finally:
|
||||
_RACE_PROTECTION = False
|
||||
|
||||
|
||||
def find_related_module(app, related_name):
|
||||
"""Given an application name and a module name, tries to find that
|
||||
module in the application."""
|
||||
|
||||
try:
|
||||
app_path = importlib.import_module(app).__path__
|
||||
except ImportError as exc:
|
||||
warn('Autodiscover: Error importing %s.%s: %r' % (
|
||||
app, related_name, exc,
|
||||
))
|
||||
return
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
try:
|
||||
f, _, _ = imp.find_module(related_name, app_path)
|
||||
# f is returned None when app_path is a module
|
||||
f and f.close()
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
return importlib.import_module('{0}.{1}'.format(app, related_name))
|
||||
@@ -0,0 +1,142 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import celery
|
||||
import djcelery
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from djcelery.compat import setenv
|
||||
|
||||
DB_SHARED_THREAD = """\
|
||||
DatabaseWrapper objects created in a thread can only \
|
||||
be used in that same thread. The object with alias '{0}' \
|
||||
was created in thread id {1} and this is thread id {2}.\
|
||||
"""
|
||||
|
||||
|
||||
def patch_thread_ident():
|
||||
# monkey patch django.
|
||||
# This patch make sure that we use real threads to get the ident which
|
||||
# is going to happen if we are using gevent or eventlet.
|
||||
# -- patch taken from gunicorn
|
||||
if getattr(patch_thread_ident, 'called', False):
|
||||
return
|
||||
try:
|
||||
from django.db.backends import BaseDatabaseWrapper, DatabaseError
|
||||
|
||||
if 'validate_thread_sharing' in BaseDatabaseWrapper.__dict__:
|
||||
import thread
|
||||
_get_ident = thread.get_ident
|
||||
|
||||
__old__init__ = BaseDatabaseWrapper.__init__
|
||||
|
||||
def _init(self, *args, **kwargs):
|
||||
__old__init__(self, *args, **kwargs)
|
||||
self._thread_ident = _get_ident()
|
||||
|
||||
def _validate_thread_sharing(self):
|
||||
if (not self.allow_thread_sharing and
|
||||
self._thread_ident != _get_ident()):
|
||||
raise DatabaseError(
|
||||
DB_SHARED_THREAD % (
|
||||
self.alias, self._thread_ident, _get_ident()),
|
||||
)
|
||||
|
||||
BaseDatabaseWrapper.__init__ = _init
|
||||
BaseDatabaseWrapper.validate_thread_sharing = \
|
||||
_validate_thread_sharing
|
||||
|
||||
patch_thread_ident.called = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
patch_thread_ident()
|
||||
|
||||
|
||||
class CeleryCommand(BaseCommand):
|
||||
options = ()
|
||||
if hasattr(BaseCommand, 'option_list'):
|
||||
options = BaseCommand.option_list
|
||||
else:
|
||||
def add_arguments(self, parser):
|
||||
option_typemap = {
|
||||
"string": str,
|
||||
"int": int,
|
||||
"float": float
|
||||
}
|
||||
for opt in self.option_list:
|
||||
option = {k: v
|
||||
for k, v in opt.__dict__.items()
|
||||
if v is not None}
|
||||
flags = (option.get("_long_opts", []) +
|
||||
option.get("_short_opts", []))
|
||||
if option.get('default') == ('NO', 'DEFAULT'):
|
||||
option['default'] = None
|
||||
if option.get("nargs") == 1:
|
||||
del option["nargs"]
|
||||
del option["_long_opts"]
|
||||
del option["_short_opts"]
|
||||
if "type" in option:
|
||||
opttype = option["type"]
|
||||
option["type"] = option_typemap.get(opttype, opttype)
|
||||
parser.add_argument(*flags, **option)
|
||||
|
||||
skip_opts = ['--app', '--loader', '--config', '--no-color']
|
||||
requires_system_checks = False
|
||||
keep_base_opts = False
|
||||
stdout, stderr = sys.stdout, sys.stderr
|
||||
|
||||
def get_version(self):
|
||||
return 'celery {c.__version__}\ndjango-celery {d.__version__}'.format(
|
||||
c=celery, d=djcelery,
|
||||
)
|
||||
|
||||
def execute(self, *args, **options):
|
||||
broker = options.get('broker')
|
||||
if broker:
|
||||
self.set_broker(broker)
|
||||
super(CeleryCommand, self).execute(*args, **options)
|
||||
|
||||
def set_broker(self, broker):
|
||||
setenv('CELERY_BROKER_URL', broker)
|
||||
|
||||
def run_from_argv(self, argv):
|
||||
self.handle_default_options(argv[2:])
|
||||
return super(CeleryCommand, self).run_from_argv(argv)
|
||||
|
||||
def handle_default_options(self, argv):
|
||||
acc = []
|
||||
broker = None
|
||||
for i, arg in enumerate(argv):
|
||||
# --settings and --pythonpath are also handled
|
||||
# by BaseCommand.handle_default_options, but that is
|
||||
# called with the resulting options parsed by optparse.
|
||||
if '--settings=' in arg:
|
||||
_, settings_module = arg.split('=')
|
||||
setenv('DJANGO_SETTINGS_MODULE', settings_module)
|
||||
elif '--pythonpath=' in arg:
|
||||
_, pythonpath = arg.split('=')
|
||||
sys.path.insert(0, pythonpath)
|
||||
elif '--broker=' in arg:
|
||||
_, broker = arg.split('=')
|
||||
elif arg == '-b':
|
||||
broker = argv[i + 1]
|
||||
else:
|
||||
acc.append(arg)
|
||||
if broker:
|
||||
self.set_broker(broker)
|
||||
return argv if self.keep_base_opts else acc
|
||||
|
||||
def die(self, msg):
|
||||
sys.stderr.write(msg)
|
||||
sys.stderr.write('\n')
|
||||
sys.exit()
|
||||
|
||||
def _is_unwanted_option(self, option):
|
||||
return option._long_opts and option._long_opts[0] in self.skip_opts
|
||||
|
||||
@property
|
||||
def option_list(self):
|
||||
return [x for x in self.options if not self._is_unwanted_option(x)]
|
||||
@@ -0,0 +1,22 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery.bin import celery
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.management.base import CeleryCommand
|
||||
|
||||
base = celery.CeleryCommand(app=app)
|
||||
|
||||
|
||||
class Command(CeleryCommand):
|
||||
"""The celery command."""
|
||||
help = 'celery commands, see celery help'
|
||||
options = (CeleryCommand.options +
|
||||
base.get_options() +
|
||||
base.preload_options)
|
||||
|
||||
def run_from_argv(self, argv):
|
||||
argv = self.handle_default_options(argv)
|
||||
base.execute_from_commandline(
|
||||
['{0[0]} {0[1]}'.format(argv)] + argv[2:],
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
|
||||
Start the celery clock service from the Django management command.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery.bin import beat
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.management.base import CeleryCommand
|
||||
|
||||
beat = beat.beat(app=app)
|
||||
|
||||
|
||||
class Command(CeleryCommand):
|
||||
"""Run the celery periodic task scheduler."""
|
||||
options = (CeleryCommand.options +
|
||||
beat.get_options() +
|
||||
beat.preload_options)
|
||||
help = 'Old alias to the "celery beat" command.'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
beat.run(*args, **options)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
|
||||
Shortcut to the Django snapshot service.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery.bin import events
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.management.base import CeleryCommand
|
||||
|
||||
ev = events.events(app=app)
|
||||
|
||||
|
||||
class Command(CeleryCommand):
|
||||
"""Run the celery curses event viewer."""
|
||||
options = (CeleryCommand.options +
|
||||
ev.get_options() +
|
||||
ev.preload_options)
|
||||
help = 'Takes snapshots of the clusters state to the database.'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Handle the management command."""
|
||||
options['camera'] = 'djcelery.snapshot.Camera'
|
||||
ev.run(*args, **options)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
|
||||
Start the celery daemon from the Django management command.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery.bin import worker
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.management.base import CeleryCommand
|
||||
|
||||
worker = worker.worker(app=app)
|
||||
|
||||
|
||||
class Command(CeleryCommand):
|
||||
"""Run the celery daemon."""
|
||||
help = 'Old alias to the "celery worker" command.'
|
||||
options = (CeleryCommand.options +
|
||||
worker.get_options() +
|
||||
worker.preload_options)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
worker.check_args(args)
|
||||
worker.run(**options)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
|
||||
Start detached worker node from the Django management utility.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from celery.bin import celeryd_detach
|
||||
|
||||
from djcelery.management.base import CeleryCommand
|
||||
|
||||
|
||||
class Command(CeleryCommand):
|
||||
"""Run the celery daemon."""
|
||||
help = 'Runs a detached Celery worker node.'
|
||||
options = celeryd_detach.OPTION_LIST
|
||||
|
||||
def run_from_argv(self, argv):
|
||||
|
||||
class detached(celeryd_detach.detached_celeryd):
|
||||
execv_argv = [os.path.abspath(sys.argv[0]), 'celery', 'worker']
|
||||
detached().execute_from_commandline(argv)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
|
||||
Utility to manage multiple worker instances.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery.bin import multi
|
||||
|
||||
from djcelery.management.base import CeleryCommand
|
||||
|
||||
|
||||
class Command(CeleryCommand):
|
||||
"""Run the celery daemon."""
|
||||
args = '[name1, [name2, [...]> [worker options]'
|
||||
help = 'Manage multiple Celery worker nodes.'
|
||||
options = ()
|
||||
keep_base_opts = True
|
||||
|
||||
def run_from_argv(self, argv):
|
||||
argv = self.handle_default_options(argv)
|
||||
argv.append('--cmd={0[0]} celeryd_detach'.format(argv))
|
||||
multi.MultiTool().execute_from_commandline(
|
||||
['{0[0]} {0[1]}'.format(argv)] + argv[2:],
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
|
||||
Start the celery clock service from the Django management command.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.management.base import CeleryCommand
|
||||
|
||||
try:
|
||||
from celerymon.bin.celerymon import MonitorCommand
|
||||
mon = MonitorCommand(app=app)
|
||||
except ImportError:
|
||||
mon = None
|
||||
|
||||
MISSING = """
|
||||
You don't have celerymon installed, please install it by running the following
|
||||
command:
|
||||
|
||||
$ pip install -U celerymon
|
||||
|
||||
or if you're still using easy_install (shame on you!)
|
||||
|
||||
$ easy_install -U celerymon
|
||||
"""
|
||||
|
||||
|
||||
class Command(CeleryCommand):
|
||||
"""Run the celery monitor."""
|
||||
options = (CeleryCommand.options +
|
||||
(mon and mon.get_options() + mon.preload_options or ()))
|
||||
help = 'Run the celery monitor'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Handle the management command."""
|
||||
if mon is None:
|
||||
sys.stderr.write(MISSING)
|
||||
else:
|
||||
mon.run(**options)
|
||||
@@ -0,0 +1,48 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from celery.bin import events
|
||||
|
||||
from django.core.management.commands import runserver
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.management.base import CeleryCommand
|
||||
|
||||
ev = events.events(app=app)
|
||||
|
||||
|
||||
class WebserverThread(threading.Thread):
|
||||
|
||||
def __init__(self, addrport='', *args, **options):
|
||||
threading.Thread.__init__(self)
|
||||
self.addrport = addrport
|
||||
self.args = args
|
||||
self.options = options
|
||||
|
||||
def run(self):
|
||||
options = dict(self.options, use_reloader=False)
|
||||
command = runserver.Command()
|
||||
# see http://code.djangoproject.com/changeset/13319
|
||||
command.stdout, command.stderr = sys.stdout, sys.stderr
|
||||
command.handle(self.addrport, *self.args, **options)
|
||||
|
||||
|
||||
class Command(CeleryCommand):
|
||||
"""Run the celery curses event viewer."""
|
||||
args = '[optional port number, or ipaddr:port]'
|
||||
options = (runserver.Command.option_list +
|
||||
ev.get_options() +
|
||||
ev.preload_options)
|
||||
help = 'Starts Django Admin instance and celerycam in the same process.'
|
||||
# see http://code.djangoproject.com/changeset/13319.
|
||||
stdout, stderr = sys.stdout, sys.stderr
|
||||
|
||||
def handle(self, addrport='', *args, **options):
|
||||
"""Handle the management command."""
|
||||
server = WebserverThread(addrport, *args, **options)
|
||||
server.start()
|
||||
options['camera'] = 'djcelery.snapshot.Camera'
|
||||
options['prog_name'] = 'djcelerymon'
|
||||
ev.run(*args, **options)
|
||||
@@ -0,0 +1,243 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import warnings
|
||||
|
||||
from functools import wraps
|
||||
from itertools import count
|
||||
|
||||
from django.db import connection
|
||||
try:
|
||||
from django.db import connections, router
|
||||
except ImportError: # pre-Django 1.2
|
||||
connections = router = None # noqa
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
from django.conf import settings
|
||||
|
||||
try:
|
||||
from celery.utils.timeutils import maybe_timedelta
|
||||
except ImportError:
|
||||
from celery.utils.time import maybe_timedelta
|
||||
|
||||
from .db import commit_on_success, get_queryset, rollback_unless_managed
|
||||
from .utils import now
|
||||
|
||||
|
||||
class TxIsolationWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
def transaction_retry(max_retries=1):
|
||||
"""Decorator for methods doing database operations.
|
||||
|
||||
If the database operation fails, it will retry the operation
|
||||
at most ``max_retries`` times.
|
||||
|
||||
"""
|
||||
def _outer(fun):
|
||||
|
||||
@wraps(fun)
|
||||
def _inner(*args, **kwargs):
|
||||
_max_retries = kwargs.pop('exception_retry_count', max_retries)
|
||||
for retries in count(0):
|
||||
try:
|
||||
return fun(*args, **kwargs)
|
||||
except Exception: # pragma: no cover
|
||||
# Depending on the database backend used we can experience
|
||||
# various exceptions. E.g. psycopg2 raises an exception
|
||||
# if some operation breaks the transaction, so saving
|
||||
# the task result won't be possible until we rollback
|
||||
# the transaction.
|
||||
if retries >= _max_retries:
|
||||
raise
|
||||
try:
|
||||
rollback_unless_managed()
|
||||
except Exception:
|
||||
pass
|
||||
return _inner
|
||||
|
||||
return _outer
|
||||
|
||||
|
||||
def update_model_with_dict(obj, fields):
|
||||
[setattr(obj, attr_name, attr_value)
|
||||
for attr_name, attr_value in fields.items()]
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
|
||||
class ExtendedQuerySet(QuerySet):
|
||||
|
||||
def update_or_create(self, **kwargs):
|
||||
obj, created = self.get_or_create(**kwargs)
|
||||
|
||||
if not created:
|
||||
fields = dict(kwargs.pop('defaults', {}))
|
||||
fields.update(kwargs)
|
||||
update_model_with_dict(obj, fields)
|
||||
|
||||
return obj, created
|
||||
|
||||
|
||||
class ExtendedManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
return ExtendedQuerySet(self.model)
|
||||
get_query_set = get_queryset # Pre django 1.6
|
||||
|
||||
def update_or_create(self, **kwargs):
|
||||
return get_queryset(self).update_or_create(**kwargs)
|
||||
|
||||
def connection_for_write(self):
|
||||
if connections:
|
||||
return connections[router.db_for_write(self.model)]
|
||||
return connection
|
||||
|
||||
def connection_for_read(self):
|
||||
if connections:
|
||||
return connections[self.db]
|
||||
return connection
|
||||
|
||||
def current_engine(self):
|
||||
try:
|
||||
return settings.DATABASES[self.db]['ENGINE']
|
||||
except AttributeError:
|
||||
return settings.DATABASE_ENGINE
|
||||
|
||||
|
||||
class ResultManager(ExtendedManager):
|
||||
|
||||
def get_all_expired(self, expires):
|
||||
"""Get all expired task results."""
|
||||
return self.filter(date_done__lt=now() - maybe_timedelta(expires))
|
||||
|
||||
def delete_expired(self, expires):
|
||||
"""Delete all expired taskset results."""
|
||||
meta = self.model._meta
|
||||
with commit_on_success():
|
||||
self.get_all_expired(expires).update(hidden=True)
|
||||
cursor = self.connection_for_write().cursor()
|
||||
cursor.execute(
|
||||
'DELETE FROM {0.db_table} WHERE hidden=%s'.format(meta),
|
||||
(True, ),
|
||||
)
|
||||
|
||||
|
||||
class PeriodicTaskManager(ExtendedManager):
|
||||
|
||||
def enabled(self):
|
||||
return self.filter(enabled=True)
|
||||
|
||||
|
||||
class TaskManager(ResultManager):
|
||||
"""Manager for :class:`celery.models.Task` models."""
|
||||
_last_id = None
|
||||
|
||||
def get_task(self, task_id):
|
||||
"""Get task meta for task by ``task_id``.
|
||||
|
||||
:keyword exception_retry_count: How many times to retry by
|
||||
transaction rollback on exception. This could theoretically
|
||||
happen in a race condition if another worker is trying to
|
||||
create the same task. The default is to retry once.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self.get(task_id=task_id)
|
||||
except self.model.DoesNotExist:
|
||||
if self._last_id == task_id:
|
||||
self.warn_if_repeatable_read()
|
||||
self._last_id = task_id
|
||||
return self.model(task_id=task_id)
|
||||
|
||||
@transaction_retry(max_retries=2)
|
||||
def store_result(self, task_id, result, status,
|
||||
traceback=None, children=None):
|
||||
"""Store the result and status of a task.
|
||||
|
||||
:param task_id: task id
|
||||
|
||||
:param result: The return value of the task, or an exception
|
||||
instance raised by the task.
|
||||
|
||||
:param status: Task status. See
|
||||
:meth:`celery.result.AsyncResult.get_status` for a list of
|
||||
possible status values.
|
||||
|
||||
:keyword traceback: The traceback at the point of exception (if the
|
||||
task failed).
|
||||
|
||||
:keyword children: List of serialized results of subtasks
|
||||
of this task.
|
||||
|
||||
:keyword exception_retry_count: How many times to retry by
|
||||
transaction rollback on exception. This could theoretically
|
||||
happen in a race condition if another worker is trying to
|
||||
create the same task. The default is to retry twice.
|
||||
|
||||
"""
|
||||
return self.update_or_create(task_id=task_id,
|
||||
defaults={'status': status,
|
||||
'result': result,
|
||||
'traceback': traceback,
|
||||
'meta': {'children': children}})
|
||||
|
||||
def warn_if_repeatable_read(self):
|
||||
if 'mysql' in self.current_engine().lower():
|
||||
cursor = self.connection_for_read().cursor()
|
||||
if cursor.execute('SELECT @@tx_isolation'):
|
||||
isolation = cursor.fetchone()[0]
|
||||
if isolation == 'REPEATABLE-READ':
|
||||
warnings.warn(TxIsolationWarning(
|
||||
'Polling results with transaction isolation level '
|
||||
'repeatable-read within the same transaction '
|
||||
'may give outdated results. Be sure to commit the '
|
||||
'transaction for each poll iteration.'))
|
||||
|
||||
|
||||
class TaskSetManager(ResultManager):
|
||||
"""Manager for :class:`celery.models.TaskSet` models."""
|
||||
|
||||
def restore_taskset(self, taskset_id):
|
||||
"""Get the async result instance by taskset id."""
|
||||
try:
|
||||
return self.get(taskset_id=taskset_id)
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
def delete_taskset(self, taskset_id):
|
||||
"""Delete a saved taskset result."""
|
||||
s = self.restore_taskset(taskset_id)
|
||||
if s:
|
||||
s.delete()
|
||||
|
||||
@transaction_retry(max_retries=2)
|
||||
def store_result(self, taskset_id, result):
|
||||
"""Store the async result instance of a taskset.
|
||||
|
||||
:param taskset_id: task set id
|
||||
|
||||
:param result: The return value of the taskset
|
||||
|
||||
"""
|
||||
return self.update_or_create(taskset_id=taskset_id,
|
||||
defaults={'result': result})
|
||||
|
||||
|
||||
class TaskStateManager(ExtendedManager):
|
||||
|
||||
def active(self):
|
||||
return self.filter(hidden=False)
|
||||
|
||||
def expired(self, states, expires, nowfun=now):
|
||||
return self.filter(state__in=states,
|
||||
tstamp__lte=nowfun() - maybe_timedelta(expires))
|
||||
|
||||
def expire_by_states(self, states, expires):
|
||||
if expires is not None:
|
||||
return self.expired(states, expires).update(hidden=True)
|
||||
|
||||
def purge(self):
|
||||
with commit_on_success():
|
||||
self.model.objects.filter(hidden=True).delete()
|
||||
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import djcelery.picklefield
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CrontabSchedule',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('minute', models.CharField(default='*', max_length=64, verbose_name='minute')),
|
||||
('hour', models.CharField(default='*', max_length=64, verbose_name='hour')),
|
||||
('day_of_week', models.CharField(default='*', max_length=64, verbose_name='day of week')),
|
||||
('day_of_month', models.CharField(default='*', max_length=64, verbose_name='day of month')),
|
||||
('month_of_year', models.CharField(default='*', max_length=64, verbose_name='month of year')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['month_of_year', 'day_of_month', 'day_of_week', 'hour', 'minute'],
|
||||
'verbose_name': 'crontab',
|
||||
'verbose_name_plural': 'crontabs',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IntervalSchedule',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('every', models.IntegerField(verbose_name='every')),
|
||||
('period', models.CharField(max_length=24, verbose_name='period', choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes'), ('seconds', 'Seconds'), ('microseconds', 'Microseconds')])),
|
||||
],
|
||||
options={
|
||||
'ordering': ['period', 'every'],
|
||||
'verbose_name': 'interval',
|
||||
'verbose_name_plural': 'intervals',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PeriodicTask',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(help_text='Useful description', unique=True, max_length=200, verbose_name='name')),
|
||||
('task', models.CharField(max_length=200, verbose_name='task name')),
|
||||
('args', models.TextField(default='[]', help_text='JSON encoded positional arguments', verbose_name='Arguments', blank=True)),
|
||||
('kwargs', models.TextField(default='{}', help_text='JSON encoded keyword arguments', verbose_name='Keyword arguments', blank=True)),
|
||||
('queue', models.CharField(default=None, max_length=200, blank=True, help_text='Queue defined in CELERY_QUEUES', null=True, verbose_name='queue')),
|
||||
('exchange', models.CharField(default=None, max_length=200, null=True, verbose_name='exchange', blank=True)),
|
||||
('routing_key', models.CharField(default=None, max_length=200, null=True, verbose_name='routing key', blank=True)),
|
||||
('expires', models.DateTimeField(null=True, verbose_name='expires', blank=True)),
|
||||
('enabled', models.BooleanField(default=True, verbose_name='enabled')),
|
||||
('last_run_at', models.DateTimeField(null=True, editable=False, blank=True)),
|
||||
('total_run_count', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('date_changed', models.DateTimeField(auto_now=True)),
|
||||
('description', models.TextField(verbose_name='description', blank=True)),
|
||||
('crontab', models.ForeignKey(blank=True, to='djcelery.CrontabSchedule', help_text='Use one of interval/crontab', null=True, verbose_name='crontab', on_delete=models.CASCADE)),
|
||||
('interval', models.ForeignKey(verbose_name='interval', blank=True, to='djcelery.IntervalSchedule', null=True, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'periodic task',
|
||||
'verbose_name_plural': 'periodic tasks',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PeriodicTasks',
|
||||
fields=[
|
||||
('ident', models.SmallIntegerField(default=1, unique=True, serialize=False, primary_key=True)),
|
||||
('last_update', models.DateTimeField()),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TaskMeta',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('task_id', models.CharField(unique=True, max_length=255, verbose_name='task id')),
|
||||
('status', models.CharField(default='PENDING', max_length=50, verbose_name='state', choices=[('FAILURE', 'FAILURE'), ('PENDING', 'PENDING'), ('RECEIVED', 'RECEIVED'), ('RETRY', 'RETRY'), ('REVOKED', 'REVOKED'), ('STARTED', 'STARTED'), ('SUCCESS', 'SUCCESS')])),
|
||||
('result', djcelery.picklefield.PickledObjectField(default=None, null=True, editable=False)),
|
||||
('date_done', models.DateTimeField(auto_now=True, verbose_name='done at')),
|
||||
('traceback', models.TextField(null=True, verbose_name='traceback', blank=True)),
|
||||
('hidden', models.BooleanField(default=False, db_index=True, editable=False)),
|
||||
('meta', djcelery.picklefield.PickledObjectField(default=None, null=True, editable=False)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'celery_taskmeta',
|
||||
'verbose_name': 'task state',
|
||||
'verbose_name_plural': 'task states',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TaskSetMeta',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('taskset_id', models.CharField(unique=True, max_length=255, verbose_name='group id')),
|
||||
('result', djcelery.picklefield.PickledObjectField(editable=False)),
|
||||
('date_done', models.DateTimeField(auto_now=True, verbose_name='created at')),
|
||||
('hidden', models.BooleanField(default=False, db_index=True, editable=False)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'celery_tasksetmeta',
|
||||
'verbose_name': 'saved group result',
|
||||
'verbose_name_plural': 'saved group results',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TaskState',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('state', models.CharField(db_index=True, max_length=64, verbose_name='state', choices=[('FAILURE', 'FAILURE'), ('PENDING', 'PENDING'), ('RECEIVED', 'RECEIVED'), ('RETRY', 'RETRY'), ('REVOKED', 'REVOKED'), ('STARTED', 'STARTED'), ('SUCCESS', 'SUCCESS')])),
|
||||
('task_id', models.CharField(unique=True, max_length=36, verbose_name='UUID')),
|
||||
('name', models.CharField(max_length=200, null=True, verbose_name='name', db_index=True)),
|
||||
('tstamp', models.DateTimeField(verbose_name='event received at', db_index=True)),
|
||||
('args', models.TextField(null=True, verbose_name='Arguments')),
|
||||
('kwargs', models.TextField(null=True, verbose_name='Keyword arguments')),
|
||||
('eta', models.DateTimeField(null=True, verbose_name='ETA')),
|
||||
('expires', models.DateTimeField(null=True, verbose_name='expires')),
|
||||
('result', models.TextField(null=True, verbose_name='result')),
|
||||
('traceback', models.TextField(null=True, verbose_name='traceback')),
|
||||
('runtime', models.FloatField(help_text='in seconds if task succeeded', null=True, verbose_name='execution time')),
|
||||
('retries', models.IntegerField(default=0, verbose_name='number of retries')),
|
||||
('hidden', models.BooleanField(default=False, db_index=True, editable=False)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-tstamp'],
|
||||
'get_latest_by': 'tstamp',
|
||||
'verbose_name': 'task',
|
||||
'verbose_name_plural': 'tasks',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WorkerState',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('hostname', models.CharField(unique=True, max_length=255, verbose_name='hostname')),
|
||||
('last_heartbeat', models.DateTimeField(null=True, verbose_name='last heartbeat', db_index=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-last_heartbeat'],
|
||||
'get_latest_by': 'last_heartbeat',
|
||||
'verbose_name': 'worker',
|
||||
'verbose_name_plural': 'workers',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='taskstate',
|
||||
name='worker',
|
||||
field=models.ForeignKey(verbose_name='worker', to='djcelery.WorkerState', null=True, on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,381 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import timedelta
|
||||
from time import time, mktime, gmtime
|
||||
|
||||
from django.core.exceptions import MultipleObjectsReturned, ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import signals
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from celery import schedules
|
||||
from celery import states
|
||||
from celery.events.state import heartbeat_expires
|
||||
|
||||
from . import managers
|
||||
from .picklefield import PickledObjectField
|
||||
from .utils import now
|
||||
from .compat import python_2_unicode_compatible
|
||||
|
||||
ALL_STATES = sorted(states.ALL_STATES)
|
||||
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
||||
|
||||
|
||||
def cronexp(field):
|
||||
return field and str(field).replace(' ', '') or '*'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TaskMeta(models.Model):
|
||||
"""Task result/status."""
|
||||
task_id = models.CharField(_('task id'), max_length=255, unique=True)
|
||||
status = models.CharField(
|
||||
_('state'),
|
||||
max_length=50, default=states.PENDING, choices=TASK_STATE_CHOICES,
|
||||
)
|
||||
result = PickledObjectField(null=True, default=None, editable=False)
|
||||
date_done = models.DateTimeField(_('done at'), auto_now=True)
|
||||
traceback = models.TextField(_('traceback'), blank=True, null=True)
|
||||
hidden = models.BooleanField(editable=False, default=False, db_index=True)
|
||||
# TODO compression was enabled by mistake, we need to disable it
|
||||
# but this is a backwards incompatible change that needs planning.
|
||||
meta = PickledObjectField(
|
||||
compress=True, null=True, default=None, editable=False,
|
||||
)
|
||||
|
||||
objects = managers.TaskManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('task state')
|
||||
verbose_name_plural = _('task states')
|
||||
db_table = 'celery_taskmeta'
|
||||
|
||||
def to_dict(self):
|
||||
return {'task_id': self.task_id,
|
||||
'status': self.status,
|
||||
'result': self.result,
|
||||
'date_done': self.date_done,
|
||||
'traceback': self.traceback,
|
||||
'children': (self.meta or {}).get('children')}
|
||||
|
||||
def __str__(self):
|
||||
return '<Task: {0.task_id} state={0.status}>'.format(self)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TaskSetMeta(models.Model):
|
||||
"""TaskSet result"""
|
||||
taskset_id = models.CharField(_('group id'), max_length=255, unique=True)
|
||||
result = PickledObjectField()
|
||||
date_done = models.DateTimeField(_('created at'), auto_now=True)
|
||||
hidden = models.BooleanField(editable=False, default=False, db_index=True)
|
||||
|
||||
objects = managers.TaskSetManager()
|
||||
|
||||
class Meta:
|
||||
"""Model meta-data."""
|
||||
verbose_name = _('saved group result')
|
||||
verbose_name_plural = _('saved group results')
|
||||
db_table = 'celery_tasksetmeta'
|
||||
|
||||
def to_dict(self):
|
||||
return {'taskset_id': self.taskset_id,
|
||||
'result': self.result,
|
||||
'date_done': self.date_done}
|
||||
|
||||
def __str__(self):
|
||||
return '<TaskSet: {0.taskset_id}>'.format(self)
|
||||
|
||||
|
||||
PERIOD_CHOICES = (('days', _('Days')),
|
||||
('hours', _('Hours')),
|
||||
('minutes', _('Minutes')),
|
||||
('seconds', _('Seconds')),
|
||||
('microseconds', _('Microseconds')))
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class IntervalSchedule(models.Model):
|
||||
every = models.IntegerField(_('every'), null=False)
|
||||
period = models.CharField(
|
||||
_('period'), max_length=24, choices=PERIOD_CHOICES,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('interval')
|
||||
verbose_name_plural = _('intervals')
|
||||
ordering = ['period', 'every']
|
||||
|
||||
@property
|
||||
def schedule(self):
|
||||
return schedules.schedule(timedelta(**{self.period: self.every}))
|
||||
|
||||
@classmethod
|
||||
def from_schedule(cls, schedule, period='seconds'):
|
||||
every = max(schedule.run_every.total_seconds(), 0)
|
||||
try:
|
||||
return cls.objects.get(every=every, period=period)
|
||||
except cls.DoesNotExist:
|
||||
return cls(every=every, period=period)
|
||||
except MultipleObjectsReturned:
|
||||
cls.objects.filter(every=every, period=period).delete()
|
||||
return cls(every=every, period=period)
|
||||
|
||||
def __str__(self):
|
||||
if self.every == 1:
|
||||
return _('every {0.period_singular}').format(self)
|
||||
return _('every {0.every:d} {0.period}').format(self)
|
||||
|
||||
@property
|
||||
def period_singular(self):
|
||||
return self.period[:-1]
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CrontabSchedule(models.Model):
|
||||
minute = models.CharField(_('minute'), max_length=64, default='*')
|
||||
hour = models.CharField(_('hour'), max_length=64, default='*')
|
||||
day_of_week = models.CharField(
|
||||
_('day of week'), max_length=64, default='*',
|
||||
)
|
||||
day_of_month = models.CharField(
|
||||
_('day of month'), max_length=64, default='*',
|
||||
)
|
||||
month_of_year = models.CharField(
|
||||
_('month of year'), max_length=64, default='*',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('crontab')
|
||||
verbose_name_plural = _('crontabs')
|
||||
ordering = ['month_of_year', 'day_of_month',
|
||||
'day_of_week', 'hour', 'minute']
|
||||
|
||||
def __str__(self):
|
||||
return '{0} {1} {2} {3} {4} (m/h/d/dM/MY)'.format(
|
||||
cronexp(self.minute),
|
||||
cronexp(self.hour),
|
||||
cronexp(self.day_of_week),
|
||||
cronexp(self.day_of_month),
|
||||
cronexp(self.month_of_year),
|
||||
)
|
||||
|
||||
@property
|
||||
def schedule(self):
|
||||
return schedules.crontab(minute=self.minute,
|
||||
hour=self.hour,
|
||||
day_of_week=self.day_of_week,
|
||||
day_of_month=self.day_of_month,
|
||||
month_of_year=self.month_of_year)
|
||||
|
||||
@classmethod
|
||||
def from_schedule(cls, schedule):
|
||||
spec = {'minute': schedule._orig_minute,
|
||||
'hour': schedule._orig_hour,
|
||||
'day_of_week': schedule._orig_day_of_week,
|
||||
'day_of_month': schedule._orig_day_of_month,
|
||||
'month_of_year': schedule._orig_month_of_year}
|
||||
try:
|
||||
return cls.objects.get(**spec)
|
||||
except cls.DoesNotExist:
|
||||
return cls(**spec)
|
||||
except MultipleObjectsReturned:
|
||||
cls.objects.filter(**spec).delete()
|
||||
return cls(**spec)
|
||||
|
||||
|
||||
class PeriodicTasks(models.Model):
|
||||
ident = models.SmallIntegerField(default=1, primary_key=True, unique=True)
|
||||
last_update = models.DateTimeField(null=False)
|
||||
|
||||
objects = managers.ExtendedManager()
|
||||
|
||||
@classmethod
|
||||
def changed(cls, instance, **kwargs):
|
||||
if not instance.no_changes:
|
||||
cls.objects.update_or_create(ident=1,
|
||||
defaults={'last_update': now()})
|
||||
|
||||
@classmethod
|
||||
def last_change(cls):
|
||||
try:
|
||||
return cls.objects.get(ident=1).last_update
|
||||
except cls.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PeriodicTask(models.Model):
|
||||
name = models.CharField(
|
||||
_('name'), max_length=200, unique=True,
|
||||
help_text=_('Useful description'),
|
||||
)
|
||||
task = models.CharField(_('task name'), max_length=200)
|
||||
interval = models.ForeignKey(
|
||||
IntervalSchedule,
|
||||
null=True, blank=True, verbose_name=_('interval'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
crontab = models.ForeignKey(
|
||||
CrontabSchedule, null=True, blank=True, verbose_name=_('crontab'),
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_('Use one of interval/crontab'),
|
||||
)
|
||||
args = models.TextField(
|
||||
_('Arguments'), blank=True, default='[]',
|
||||
help_text=_('JSON encoded positional arguments'),
|
||||
)
|
||||
kwargs = models.TextField(
|
||||
_('Keyword arguments'), blank=True, default='{}',
|
||||
help_text=_('JSON encoded keyword arguments'),
|
||||
)
|
||||
queue = models.CharField(
|
||||
_('queue'), max_length=200, blank=True, null=True, default=None,
|
||||
help_text=_('Queue defined in CELERY_QUEUES'),
|
||||
)
|
||||
exchange = models.CharField(
|
||||
_('exchange'), max_length=200, blank=True, null=True, default=None,
|
||||
)
|
||||
routing_key = models.CharField(
|
||||
_('routing key'), max_length=200, blank=True, null=True, default=None,
|
||||
)
|
||||
expires = models.DateTimeField(
|
||||
_('expires'), blank=True, null=True,
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
_('enabled'), default=True,
|
||||
)
|
||||
last_run_at = models.DateTimeField(
|
||||
auto_now=False, auto_now_add=False,
|
||||
editable=False, blank=True, null=True,
|
||||
)
|
||||
total_run_count = models.PositiveIntegerField(
|
||||
default=0, editable=False,
|
||||
)
|
||||
date_changed = models.DateTimeField(auto_now=True)
|
||||
description = models.TextField(_('description'), blank=True)
|
||||
|
||||
objects = managers.PeriodicTaskManager()
|
||||
no_changes = False
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('periodic task')
|
||||
verbose_name_plural = _('periodic tasks')
|
||||
|
||||
def validate_unique(self, *args, **kwargs):
|
||||
super(PeriodicTask, self).validate_unique(*args, **kwargs)
|
||||
if not self.interval and not self.crontab:
|
||||
raise ValidationError(
|
||||
{'interval': ['One of interval or crontab must be set.']})
|
||||
if self.interval and self.crontab:
|
||||
raise ValidationError(
|
||||
{'crontab': ['Only one of interval or crontab must be set']})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.exchange = self.exchange or None
|
||||
self.routing_key = self.routing_key or None
|
||||
self.queue = self.queue or None
|
||||
if not self.enabled:
|
||||
self.last_run_at = None
|
||||
super(PeriodicTask, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
fmt = '{0.name}: {{no schedule}}'
|
||||
if self.interval:
|
||||
fmt = '{0.name}: {0.interval}'
|
||||
if self.crontab:
|
||||
fmt = '{0.name}: {0.crontab}'
|
||||
return fmt.format(self)
|
||||
|
||||
@property
|
||||
def schedule(self):
|
||||
if self.interval:
|
||||
return self.interval.schedule
|
||||
if self.crontab:
|
||||
return self.crontab.schedule
|
||||
|
||||
|
||||
signals.pre_delete.connect(PeriodicTasks.changed, sender=PeriodicTask)
|
||||
signals.pre_save.connect(PeriodicTasks.changed, sender=PeriodicTask)
|
||||
|
||||
|
||||
class WorkerState(models.Model):
|
||||
hostname = models.CharField(_('hostname'), max_length=255, unique=True)
|
||||
last_heartbeat = models.DateTimeField(_('last heartbeat'), null=True,
|
||||
db_index=True)
|
||||
|
||||
objects = managers.ExtendedManager()
|
||||
|
||||
class Meta:
|
||||
"""Model meta-data."""
|
||||
verbose_name = _('worker')
|
||||
verbose_name_plural = _('workers')
|
||||
get_latest_by = 'last_heartbeat'
|
||||
ordering = ['-last_heartbeat']
|
||||
|
||||
def __str__(self):
|
||||
return self.hostname
|
||||
|
||||
def __repr__(self):
|
||||
return '<WorkerState: {0.hostname}>'.format(self)
|
||||
|
||||
def is_alive(self):
|
||||
if self.last_heartbeat:
|
||||
# Use UTC timestamp if USE_TZ is true, or else use local timestamp
|
||||
timestamp = mktime(gmtime()) if settings.USE_TZ else time()
|
||||
return timestamp < heartbeat_expires(self.heartbeat_timestamp)
|
||||
return False
|
||||
|
||||
@property
|
||||
def heartbeat_timestamp(self):
|
||||
return mktime(self.last_heartbeat.timetuple())
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TaskState(models.Model):
|
||||
state = models.CharField(
|
||||
_('state'), max_length=64, choices=TASK_STATE_CHOICES, db_index=True,
|
||||
)
|
||||
task_id = models.CharField(_('UUID'), max_length=36, unique=True)
|
||||
name = models.CharField(
|
||||
_('name'), max_length=200, null=True, db_index=True,
|
||||
)
|
||||
tstamp = models.DateTimeField(_('event received at'), db_index=True)
|
||||
args = models.TextField(_('Arguments'), null=True)
|
||||
kwargs = models.TextField(_('Keyword arguments'), null=True)
|
||||
eta = models.DateTimeField(_('ETA'), null=True)
|
||||
expires = models.DateTimeField(_('expires'), null=True)
|
||||
result = models.TextField(_('result'), null=True)
|
||||
traceback = models.TextField(_('traceback'), null=True)
|
||||
runtime = models.FloatField(
|
||||
_('execution time'), null=True,
|
||||
help_text=_('in seconds if task succeeded'),
|
||||
)
|
||||
retries = models.IntegerField(_('number of retries'), default=0)
|
||||
worker = models.ForeignKey(
|
||||
WorkerState, null=True, verbose_name=_('worker'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
hidden = models.BooleanField(editable=False, default=False, db_index=True)
|
||||
|
||||
objects = managers.TaskStateManager()
|
||||
|
||||
class Meta:
|
||||
"""Model meta-data."""
|
||||
verbose_name = _('task')
|
||||
verbose_name_plural = _('tasks')
|
||||
get_latest_by = 'tstamp'
|
||||
ordering = ['-tstamp']
|
||||
|
||||
def __str__(self):
|
||||
name = self.name or 'UNKNOWN'
|
||||
s = '{0.state:<10} {0.task_id:<36} {1}'.format(self, name)
|
||||
if self.eta:
|
||||
s += ' eta:{0.eta}'.format(self)
|
||||
return s
|
||||
|
||||
def __repr__(self):
|
||||
return '<TaskState: {0.state} {1}[{0.task_id}] ts:{0.tstamp}>'.format(
|
||||
self, self.name or 'UNKNOWN',
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
from celery.app.defaults import strtobool
|
||||
from celery.utils import import_from_cwd
|
||||
|
||||
from djcelery.compat import setenv
|
||||
|
||||
DEFAULT_APPS = ('django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.admindocs',
|
||||
'djcelery',
|
||||
)
|
||||
|
||||
DEFAULTS = {'ROOT_URLCONF': 'djcelery.monproj.urls',
|
||||
'DATABASE_ENGINE': 'sqlite3',
|
||||
'DATABASE_NAME': 'djcelerymon.db',
|
||||
'DATABASES': {'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'djcelerymon.db'}},
|
||||
'BROKER_URL': 'amqp://',
|
||||
'SITE_ID': 1,
|
||||
'INSTALLED_APPS': DEFAULT_APPS,
|
||||
'DEBUG': strtobool(os.environ.get('DJCELERYMON_DEBUG', '0'))
|
||||
}
|
||||
|
||||
|
||||
def default_settings(name='__default_settings__'):
|
||||
c = type(name, (types.ModuleType, ), DEFAULTS)(name)
|
||||
c.__dict__.update({'__file__': __file__})
|
||||
sys.modules[name] = c
|
||||
return name
|
||||
|
||||
|
||||
def configure():
|
||||
from celery import current_app
|
||||
from celery.loaders.default import DEFAULT_CONFIG_MODULE
|
||||
from django.conf import settings
|
||||
|
||||
app = current_app
|
||||
conf = {}
|
||||
|
||||
if not settings.configured:
|
||||
if 'loader' in app.__dict__ and app.loader.configured:
|
||||
conf = current_app.loader.conf
|
||||
else:
|
||||
os.environ.pop('CELERY_LOADER', None)
|
||||
settings_module = os.environ.get('CELERY_CONFIG_MODULE',
|
||||
DEFAULT_CONFIG_MODULE)
|
||||
try:
|
||||
import_from_cwd(settings_module)
|
||||
except ImportError:
|
||||
settings_module = default_settings()
|
||||
settings.configure(SETTINGS_MODULE=settings_module,
|
||||
**dict(DEFAULTS, **conf))
|
||||
|
||||
|
||||
def run_monitor(argv):
|
||||
from .management.commands import djcelerymon
|
||||
djcelerymon.Command().run_from_argv([argv[0], 'djcelerymon'] + argv[1:])
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
from django.core import management
|
||||
setenv('CELERY_LOADER', 'default')
|
||||
configure()
|
||||
management.call_command('migrate')
|
||||
run_monitor(argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
|
||||
# to INSTALLED_APPS to enable admin documentation:
|
||||
url(
|
||||
r'^doc/',
|
||||
include('django.contrib.admindocs.urls')
|
||||
),
|
||||
|
||||
url(r'', include(admin.site.urls)),
|
||||
]
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Based on django-picklefield which is
|
||||
Copyright (c) 2009-2010 Gintautas Miliauskas
|
||||
but some improvements including not deepcopying values.
|
||||
|
||||
Provides an implementation of a pickled object field.
|
||||
Such fields can contain any picklable objects.
|
||||
|
||||
The implementation is taken and adopted from Django snippet #1694
|
||||
<http://www.djangosnippets.org/snippets/1694/> by Taavi Taijala,
|
||||
which is in turn based on Django snippet #513
|
||||
<http://www.djangosnippets.org/snippets/513/> by Oliver Beattie.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import django
|
||||
|
||||
from base64 import b64encode, b64decode
|
||||
from zlib import compress, decompress
|
||||
|
||||
from celery.five import with_metaclass
|
||||
from celery.utils.serialization import pickle
|
||||
from kombu.utils.encoding import bytes_to_str, str_to_bytes
|
||||
|
||||
from django.db import models
|
||||
|
||||
try:
|
||||
from django.utils.encoding import force_text
|
||||
except ImportError:
|
||||
from django.utils.encoding import force_unicode as force_text # noqa
|
||||
|
||||
DEFAULT_PROTOCOL = 2
|
||||
|
||||
NO_DECOMPRESS_HEADER = b'\x1e\x00r8d9qwwerwhA@'
|
||||
|
||||
|
||||
if django.VERSION >= (1, 8):
|
||||
BaseField = models.Field
|
||||
else:
|
||||
@with_metaclass(models.SubfieldBase, skip_attrs=set([
|
||||
'db_type',
|
||||
'get_db_prep_save'
|
||||
]))
|
||||
class BaseField(models.Field): # noqa
|
||||
pass
|
||||
|
||||
|
||||
class PickledObject(str):
|
||||
pass
|
||||
|
||||
|
||||
def maybe_compress(value, do_compress=False):
|
||||
if do_compress:
|
||||
return compress(str_to_bytes(value))
|
||||
return value
|
||||
|
||||
|
||||
def maybe_decompress(value, do_decompress=False):
|
||||
if do_decompress:
|
||||
if str_to_bytes(value[:15]) != NO_DECOMPRESS_HEADER:
|
||||
return decompress(str_to_bytes(value))
|
||||
return value
|
||||
|
||||
|
||||
def encode(value, compress_object=False, pickle_protocol=DEFAULT_PROTOCOL):
|
||||
return bytes_to_str(b64encode(maybe_compress(
|
||||
pickle.dumps(value, pickle_protocol), compress_object),
|
||||
))
|
||||
|
||||
|
||||
def decode(value, compress_object=False):
|
||||
return pickle.loads(maybe_decompress(b64decode(value), compress_object))
|
||||
|
||||
|
||||
class PickledObjectField(BaseField):
|
||||
|
||||
def __init__(self, compress=False, protocol=DEFAULT_PROTOCOL,
|
||||
*args, **kwargs):
|
||||
self.compress = compress
|
||||
self.protocol = protocol
|
||||
kwargs.setdefault('editable', False)
|
||||
super(PickledObjectField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_default(self):
|
||||
if self.has_default():
|
||||
return self.default() if callable(self.default) else self.default
|
||||
return super(PickledObjectField, self).get_default()
|
||||
|
||||
def to_python(self, value):
|
||||
if value is not None:
|
||||
try:
|
||||
return decode(value, self.compress)
|
||||
except Exception:
|
||||
if isinstance(value, PickledObject):
|
||||
raise
|
||||
return value
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
return self.to_python(value)
|
||||
|
||||
def get_db_prep_value(self, value, **kwargs):
|
||||
if value is not None and not isinstance(value, PickledObject):
|
||||
return force_text(encode(value, self.compress, self.protocol))
|
||||
return value
|
||||
|
||||
def value_to_string(self, obj):
|
||||
return self.get_db_prep_value(self._get_val_from_obj(obj))
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'TextField'
|
||||
|
||||
def get_db_prep_lookup(self, lookup_type, value, *args, **kwargs):
|
||||
if lookup_type not in ['exact', 'in', 'isnull']:
|
||||
raise TypeError(
|
||||
'Lookup type {0} is not supported.'.format(lookup_type))
|
||||
return super(PickledObjectField, self) \
|
||||
.get_db_prep_lookup(*args, **kwargs)
|
||||
|
||||
|
||||
try:
|
||||
from south.modelsinspector import add_introspection_rules
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
add_introspection_rules(
|
||||
[], [r'^djcelery\.picklefield\.PickledObjectField'],
|
||||
)
|
||||
@@ -0,0 +1,282 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from multiprocessing.util import Finalize
|
||||
|
||||
from anyjson import loads, dumps
|
||||
from celery import current_app
|
||||
from celery import schedules
|
||||
from celery.beat import Scheduler, ScheduleEntry
|
||||
from celery.utils.encoding import safe_str, safe_repr
|
||||
from celery.utils.log import get_logger
|
||||
|
||||
try:
|
||||
from celery.utils.timeutils import is_naive
|
||||
except ImportError:
|
||||
from celery.utils.time import is_naive
|
||||
|
||||
from django.db import transaction
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from .db import commit_on_success
|
||||
from .models import (PeriodicTask, PeriodicTasks,
|
||||
CrontabSchedule, IntervalSchedule)
|
||||
from .utils import DATABASE_ERRORS, make_aware
|
||||
from .compat import itervalues
|
||||
|
||||
# This scheduler must wake up more frequently than the
|
||||
# regular of 5 minutes because it needs to take external
|
||||
# changes to the schedule into account.
|
||||
DEFAULT_MAX_INTERVAL = 5 # seconds
|
||||
|
||||
ADD_ENTRY_ERROR = """\
|
||||
Couldn't add entry %r to database schedule: %r. Contents: %r
|
||||
"""
|
||||
|
||||
logger = get_logger(__name__)
|
||||
debug, info, error = logger.debug, logger.info, logger.error
|
||||
|
||||
|
||||
class ModelEntry(ScheduleEntry):
|
||||
model_schedules = ((schedules.crontab, CrontabSchedule, 'crontab'),
|
||||
(schedules.schedule, IntervalSchedule, 'interval'))
|
||||
save_fields = ['last_run_at', 'total_run_count', 'no_changes']
|
||||
|
||||
def __init__(self, model):
|
||||
self.app = current_app._get_current_object()
|
||||
self.name = model.name
|
||||
self.task = model.task
|
||||
try:
|
||||
self.schedule = model.schedule
|
||||
except model.DoesNotExist:
|
||||
logger.error('Schedule was removed from database')
|
||||
logger.warning('Disabling %s', self.name)
|
||||
self._disable(model)
|
||||
try:
|
||||
self.args = loads(model.args or '[]')
|
||||
self.kwargs = loads(model.kwargs or '{}')
|
||||
except ValueError:
|
||||
logging.error('Failed to serialize arguments for %s.', self.name,
|
||||
exc_info=1)
|
||||
logging.warning('Disabling %s', self.name)
|
||||
self._disable(model)
|
||||
|
||||
self.options = {'queue': model.queue,
|
||||
'exchange': model.exchange,
|
||||
'routing_key': model.routing_key,
|
||||
'expires': model.expires}
|
||||
self.total_run_count = model.total_run_count
|
||||
self.model = model
|
||||
|
||||
if not model.last_run_at:
|
||||
model.last_run_at = self._default_now()
|
||||
orig = self.last_run_at = model.last_run_at
|
||||
if not is_naive(self.last_run_at):
|
||||
self.last_run_at = self.last_run_at.replace(tzinfo=None)
|
||||
assert orig.hour == self.last_run_at.hour # timezone sanity
|
||||
|
||||
def _disable(self, model):
|
||||
model.no_changes = True
|
||||
model.enabled = False
|
||||
model.save()
|
||||
|
||||
def is_due(self):
|
||||
if not self.model.enabled:
|
||||
return False, 5.0 # 5 second delay for re-enable.
|
||||
return self.schedule.is_due(self.last_run_at)
|
||||
|
||||
def _default_now(self):
|
||||
return self.app.now()
|
||||
|
||||
def __next__(self):
|
||||
self.model.last_run_at = self.app.now()
|
||||
self.model.total_run_count += 1
|
||||
self.model.no_changes = True
|
||||
return self.__class__(self.model)
|
||||
next = __next__ # for 2to3
|
||||
|
||||
def save(self):
|
||||
# Object may not be synchronized, so only
|
||||
# change the fields we care about.
|
||||
obj = type(self.model)._default_manager.get(pk=self.model.pk)
|
||||
for field in self.save_fields:
|
||||
setattr(obj, field, getattr(self.model, field))
|
||||
obj.last_run_at = make_aware(obj.last_run_at)
|
||||
obj.save()
|
||||
|
||||
@classmethod
|
||||
def to_model_schedule(cls, schedule):
|
||||
for schedule_type, model_type, model_field in cls.model_schedules:
|
||||
schedule = schedules.maybe_schedule(schedule)
|
||||
if isinstance(schedule, schedule_type):
|
||||
model_schedule = model_type.from_schedule(schedule)
|
||||
model_schedule.save()
|
||||
return model_schedule, model_field
|
||||
raise ValueError(
|
||||
'Cannot convert schedule type {0!r} to model'.format(schedule))
|
||||
|
||||
@classmethod
|
||||
def from_entry(cls, name, skip_fields=('relative', 'options'), **entry):
|
||||
options = entry.get('options') or {}
|
||||
fields = dict(entry)
|
||||
for skip_field in skip_fields:
|
||||
fields.pop(skip_field, None)
|
||||
schedule = fields.pop('schedule')
|
||||
model_schedule, model_field = cls.to_model_schedule(schedule)
|
||||
|
||||
# reset schedule
|
||||
for t in cls.model_schedules:
|
||||
fields[t[2]] = None
|
||||
|
||||
fields[model_field] = model_schedule
|
||||
fields['args'] = dumps(fields.get('args') or [])
|
||||
fields['kwargs'] = dumps(fields.get('kwargs') or {})
|
||||
fields['queue'] = options.get('queue')
|
||||
fields['exchange'] = options.get('exchange')
|
||||
fields['routing_key'] = options.get('routing_key')
|
||||
obj, _ = PeriodicTask._default_manager.update_or_create(
|
||||
name=name, defaults=fields,
|
||||
)
|
||||
return cls(obj)
|
||||
|
||||
def __repr__(self):
|
||||
return '<ModelEntry: {0} {1}(*{2}, **{3}) {4}>'.format(
|
||||
safe_str(self.name), self.task, safe_repr(self.args),
|
||||
safe_repr(self.kwargs), self.schedule,
|
||||
)
|
||||
|
||||
|
||||
class DatabaseScheduler(Scheduler):
|
||||
Entry = ModelEntry
|
||||
Model = PeriodicTask
|
||||
Changes = PeriodicTasks
|
||||
_schedule = None
|
||||
_last_timestamp = None
|
||||
_initial_read = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._dirty = set()
|
||||
self._finalize = Finalize(self, self.sync, exitpriority=5)
|
||||
Scheduler.__init__(self, *args, **kwargs)
|
||||
self.max_interval = (
|
||||
kwargs.get('max_interval') or
|
||||
self.app.conf.CELERYBEAT_MAX_LOOP_INTERVAL or
|
||||
DEFAULT_MAX_INTERVAL)
|
||||
|
||||
def setup_schedule(self):
|
||||
self.install_default_entries(self.schedule)
|
||||
self.update_from_dict(self.app.conf.CELERYBEAT_SCHEDULE)
|
||||
|
||||
def all_as_schedule(self):
|
||||
debug('DatabaseScheduler: Fetching database schedule')
|
||||
s = {}
|
||||
for model in self.Model.objects.enabled():
|
||||
try:
|
||||
s[model.name] = self.Entry(model)
|
||||
except ValueError:
|
||||
pass
|
||||
return s
|
||||
|
||||
def schedule_changed(self):
|
||||
try:
|
||||
# If MySQL is running with transaction isolation level
|
||||
# REPEATABLE-READ (default), then we won't see changes done by
|
||||
# other transactions until the current transaction is
|
||||
# committed (Issue #41).
|
||||
try:
|
||||
transaction.commit()
|
||||
except transaction.TransactionManagementError:
|
||||
pass # not in transaction management.
|
||||
|
||||
last, ts = self._last_timestamp, self.Changes.last_change()
|
||||
except DATABASE_ERRORS as exc:
|
||||
# Close the connection when it is broken
|
||||
transaction.get_connection().close_if_unusable_or_obsolete()
|
||||
error('Database gave error: %r', exc, exc_info=1)
|
||||
return False
|
||||
try:
|
||||
if ts and ts > (last if last else ts):
|
||||
return True
|
||||
finally:
|
||||
self._last_timestamp = ts
|
||||
return False
|
||||
|
||||
def reserve(self, entry):
|
||||
new_entry = Scheduler.reserve(self, entry)
|
||||
# Need to store entry by name, because the entry may change
|
||||
# in the mean time.
|
||||
self._dirty.add(new_entry.name)
|
||||
return new_entry
|
||||
|
||||
def sync(self):
|
||||
info('Writing entries (%s)...', len(self._dirty))
|
||||
_tried = set()
|
||||
try:
|
||||
with commit_on_success():
|
||||
while self._dirty:
|
||||
try:
|
||||
name = self._dirty.pop()
|
||||
_tried.add(name)
|
||||
self.schedule[name].save()
|
||||
except (KeyError, ObjectDoesNotExist):
|
||||
pass
|
||||
except DATABASE_ERRORS as exc:
|
||||
# retry later
|
||||
self._dirty |= _tried
|
||||
error('Database error while sync: %r', exc, exc_info=1)
|
||||
|
||||
def update_from_dict(self, dict_):
|
||||
s = {}
|
||||
for name, entry in dict_.items():
|
||||
try:
|
||||
s[name] = self.Entry.from_entry(name, **entry)
|
||||
except Exception as exc:
|
||||
error(ADD_ENTRY_ERROR, name, exc, entry)
|
||||
self.schedule.update(s)
|
||||
|
||||
def install_default_entries(self, data):
|
||||
entries = {}
|
||||
if self.app.conf.CELERY_TASK_RESULT_EXPIRES:
|
||||
entries.setdefault(
|
||||
'celery.backend_cleanup', {
|
||||
'task': 'celery.backend_cleanup',
|
||||
'schedule': schedules.crontab('0', '4', '*'),
|
||||
'options': {'expires': 12 * 3600},
|
||||
},
|
||||
)
|
||||
self.update_from_dict(entries)
|
||||
|
||||
@property
|
||||
def schedule(self):
|
||||
update = False
|
||||
if not self._initial_read:
|
||||
debug('DatabaseScheduler: intial read')
|
||||
update = True
|
||||
self._initial_read = True
|
||||
elif self.schedule_changed():
|
||||
info('DatabaseScheduler: Schedule changed.')
|
||||
update = True
|
||||
|
||||
if update:
|
||||
self.sync()
|
||||
self._schedule = self.all_as_schedule()
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
debug('Current schedule:\n%s', '\n'.join(
|
||||
repr(entry) for entry in itervalues(self._schedule)),
|
||||
)
|
||||
return self._schedule
|
||||
|
||||
@classmethod
|
||||
def create_or_update_task(cls, name, **schedule_dict):
|
||||
if 'schedule' not in schedule_dict:
|
||||
try:
|
||||
schedule_dict['schedule'] = \
|
||||
PeriodicTask._default_manager.get(name=name).schedule
|
||||
except PeriodicTask.DoesNotExist:
|
||||
pass
|
||||
cls.Entry.from_entry(name, **schedule_dict)
|
||||
|
||||
@classmethod
|
||||
def delete_task(cls, name):
|
||||
PeriodicTask._default_manager.get(name=name).delete()
|
||||
@@ -0,0 +1,143 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from celery import states
|
||||
from celery.events.state import Task
|
||||
from celery.events.snapshot import Polaroid
|
||||
from celery.five import monotonic
|
||||
from celery.utils.log import get_logger
|
||||
|
||||
try:
|
||||
from celery.utils.timeutils import maybe_iso8601
|
||||
except ImportError:
|
||||
from celery.utils.time import maybe_iso8601
|
||||
|
||||
from .models import WorkerState, TaskState
|
||||
from .utils import fromtimestamp, correct_awareness
|
||||
|
||||
WORKER_UPDATE_FREQ = 60 # limit worker timestamp write freq.
|
||||
SUCCESS_STATES = frozenset([states.SUCCESS])
|
||||
|
||||
# Expiry can be timedelta or None for never expire.
|
||||
EXPIRE_SUCCESS = getattr(settings, 'CELERYCAM_EXPIRE_SUCCESS',
|
||||
timedelta(days=1))
|
||||
EXPIRE_ERROR = getattr(settings, 'CELERYCAM_EXPIRE_ERROR',
|
||||
timedelta(days=3))
|
||||
EXPIRE_PENDING = getattr(settings, 'CELERYCAM_EXPIRE_PENDING',
|
||||
timedelta(days=5))
|
||||
NOT_SAVED_ATTRIBUTES = frozenset(['name', 'args', 'kwargs', 'eta'])
|
||||
|
||||
logger = get_logger(__name__)
|
||||
debug = logger.debug
|
||||
|
||||
|
||||
class Camera(Polaroid):
|
||||
TaskState = TaskState
|
||||
WorkerState = WorkerState
|
||||
|
||||
clear_after = True
|
||||
worker_update_freq = WORKER_UPDATE_FREQ
|
||||
expire_states = {
|
||||
SUCCESS_STATES: EXPIRE_SUCCESS,
|
||||
states.EXCEPTION_STATES: EXPIRE_ERROR,
|
||||
states.UNREADY_STATES: EXPIRE_PENDING,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Camera, self).__init__(*args, **kwargs)
|
||||
self._last_worker_write = defaultdict(lambda: (None, None))
|
||||
|
||||
def get_heartbeat(self, worker):
|
||||
try:
|
||||
heartbeat = worker.heartbeats[-1]
|
||||
except IndexError:
|
||||
return
|
||||
return fromtimestamp(heartbeat)
|
||||
|
||||
def handle_worker(self, hostname_worker):
|
||||
(hostname, worker) = hostname_worker
|
||||
last_write, obj = self._last_worker_write[hostname]
|
||||
if not last_write or \
|
||||
monotonic() - last_write > self.worker_update_freq:
|
||||
obj, _ = self.WorkerState.objects.update_or_create(
|
||||
hostname=hostname,
|
||||
defaults={'last_heartbeat': self.get_heartbeat(worker)},
|
||||
)
|
||||
self._last_worker_write[hostname] = (monotonic(), obj)
|
||||
return obj
|
||||
|
||||
def handle_task(self, uuid_task, worker=None):
|
||||
"""Handle snapshotted event."""
|
||||
uuid, task = uuid_task
|
||||
if task.worker and task.worker.hostname:
|
||||
worker = self.handle_worker(
|
||||
(task.worker.hostname, task.worker),
|
||||
)
|
||||
|
||||
defaults = {
|
||||
'name': task.name,
|
||||
'args': task.args,
|
||||
'kwargs': task.kwargs,
|
||||
'eta': correct_awareness(maybe_iso8601(task.eta)),
|
||||
'expires': correct_awareness(maybe_iso8601(task.expires)),
|
||||
'state': task.state,
|
||||
'tstamp': fromtimestamp(task.timestamp),
|
||||
'result': task.result or task.exception,
|
||||
'traceback': task.traceback,
|
||||
'runtime': task.runtime,
|
||||
'worker': worker
|
||||
}
|
||||
# Some fields are only stored in the RECEIVED event,
|
||||
# so we should remove these from default values,
|
||||
# so that they are not overwritten by subsequent states.
|
||||
[defaults.pop(attr, None) for attr in NOT_SAVED_ATTRIBUTES
|
||||
if defaults[attr] is None]
|
||||
return self.update_task(task.state,
|
||||
task_id=uuid, defaults=defaults)
|
||||
|
||||
def update_task(self, state, **kwargs):
|
||||
objects = self.TaskState.objects
|
||||
defaults = kwargs.pop('defaults', None) or {}
|
||||
if not defaults.get('name'):
|
||||
return
|
||||
obj, created = objects.get_or_create(defaults=defaults, **kwargs)
|
||||
if created:
|
||||
return obj
|
||||
else:
|
||||
if states.state(state) < states.state(obj.state):
|
||||
keep = Task.merge_rules[states.RECEIVED]
|
||||
defaults = dict(
|
||||
(k, v) for k, v in defaults.items()
|
||||
if k not in keep
|
||||
)
|
||||
|
||||
for k, v in defaults.items():
|
||||
setattr(obj, k, v)
|
||||
obj.save()
|
||||
|
||||
return obj
|
||||
|
||||
def on_shutter(self, state, commit_every=100):
|
||||
|
||||
def _handle_tasks():
|
||||
for i, task in enumerate(state.tasks.items()):
|
||||
self.handle_task(task)
|
||||
|
||||
for worker in state.workers.items():
|
||||
self.handle_worker(worker)
|
||||
_handle_tasks()
|
||||
|
||||
def on_cleanup(self):
|
||||
expired = (self.TaskState.objects.expire_by_states(states, expires)
|
||||
for states, expires in self.expire_states.items())
|
||||
dirty = sum(item for item in expired if item is not None)
|
||||
if dirty:
|
||||
debug('Cleanup: Marked %s objects as dirty.', dirty)
|
||||
self.TaskState.objects.purge()
|
||||
debug('Cleanup: %s objects purged.', dirty)
|
||||
return dirty
|
||||
return 0
|
||||
@@ -0,0 +1,4 @@
|
||||
.form-row.field-traceback p {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
|
||||
› {{ cl.opts.verbose_name_plural|capfirst }}
|
||||
</div>
|
||||
{% if wrong_scheduler %}
|
||||
<ul class="messagelist">
|
||||
<li class="warning">
|
||||
Periodic tasks won't be dispatched unless you set the
|
||||
<code>CELERYBEAT_SCHEDULER</code> setting to
|
||||
<code>djcelery.schedulers.DatabaseScheduler</code>,
|
||||
or specify it using the <code>-S</code> option to celerybeat
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="../../">{% trans "Home" %}</a> ›
|
||||
<a href="../">{{ app_label|capfirst }}</a> ›
|
||||
<a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
|
||||
{% trans 'Rate limit selected tasks' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
<div>
|
||||
{% for obj in queryset %}
|
||||
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}" />
|
||||
{% endfor %}
|
||||
<input type="hidden" name="action" value="rate_limit_tasks" />
|
||||
<input type="hidden" name="post" value="yes" />
|
||||
<input type="text" name="rate_limit" value="" />
|
||||
<input type="submit" value="{% trans "Rate limit" %}" />
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
# coding: utf-8
|
||||
|
||||
try:
|
||||
from unittest.mock import patch
|
||||
except ImportError:
|
||||
from mock import patch # noqa
|
||||
@@ -0,0 +1,76 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.test import Client
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.core.handlers.base import BaseHandler
|
||||
|
||||
from celery.utils.compat import WhateverIO
|
||||
|
||||
from djcelery.compat import unicode
|
||||
|
||||
|
||||
class RequestFactory(Client):
|
||||
"""Class that lets you create mock Request objects for use in testing.
|
||||
|
||||
Usage:
|
||||
|
||||
rf = RequestFactory()
|
||||
get_request = rf.get('/hello/')
|
||||
post_request = rf.post('/submit/', {'foo': 'bar'})
|
||||
|
||||
This class re-uses the django.test.client.Client interface, docs here:
|
||||
http://www.djangoproject.com/documentation/testing/#the-test-client
|
||||
|
||||
Once you have a request object you can pass it to any view function,
|
||||
just as if that view had been hooked up using a URLconf.
|
||||
|
||||
"""
|
||||
|
||||
def request(self, **request):
|
||||
"""Similar to parent class, but returns the request object as
|
||||
soon as it has created it."""
|
||||
environ = {
|
||||
'HTTP_COOKIE': unicode(self.cookies),
|
||||
'HTTP_USER_AGENT': 'Django UnitTest Client 1.0',
|
||||
'REMOTE_ADDR': '127.0.0.1',
|
||||
'PATH_INFO': '/',
|
||||
'QUERY_STRING': '',
|
||||
'REQUEST_METHOD': 'GET',
|
||||
'SCRIPT_NAME': '',
|
||||
'SERVER_NAME': 'testserver',
|
||||
'SERVER_PORT': 80,
|
||||
'SERVER_PROTOCOL': 'HTTP/1.1',
|
||||
'wsgi.input': WhateverIO(),
|
||||
}
|
||||
|
||||
environ.update(self.defaults)
|
||||
environ.update(request)
|
||||
return WSGIRequest(environ)
|
||||
|
||||
|
||||
class MockRequest(object):
|
||||
|
||||
def __init__(self):
|
||||
handler = BaseHandler()
|
||||
handler.load_middleware()
|
||||
self.request_factory = RequestFactory()
|
||||
self.middleware = handler._request_middleware
|
||||
|
||||
def _make_request(self, request_method, *args, **kwargs):
|
||||
request_method_handler = getattr(self.request_factory, request_method)
|
||||
request = request_method_handler(*args, **kwargs)
|
||||
[middleware_processor(request)
|
||||
for middleware_processor in self.middleware]
|
||||
return request
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self._make_request('get', *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self._make_request('post', *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self._make_request('put', *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self._make_request('delete', *args, **kwargs)
|
||||
@@ -0,0 +1,86 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from djcelery.admin import PeriodicTaskAdmin
|
||||
from djcelery.models import (
|
||||
PeriodicTask, IntervalSchedule, PERIOD_CHOICES, PeriodicTasks
|
||||
)
|
||||
|
||||
|
||||
class MockRequest(object):
|
||||
pass
|
||||
|
||||
|
||||
request = MockRequest()
|
||||
|
||||
site = admin.AdminSite()
|
||||
|
||||
|
||||
class TestPeriodicTaskAdmin(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.interval = IntervalSchedule.objects.create(
|
||||
every=1, period=PERIOD_CHOICES[0][0])
|
||||
|
||||
cls.request_factory = RequestFactory()
|
||||
|
||||
cls.pt_admin = PeriodicTaskAdmin(PeriodicTask, site)
|
||||
|
||||
def test_specified_ordering(self):
|
||||
"""
|
||||
Ordering should be by ('-enabled', 'name')
|
||||
"""
|
||||
PeriodicTask.objects.bulk_create([
|
||||
PeriodicTask(name='Bohemian Rhapsody', task='bohemian_rhapsody',
|
||||
interval=self.interval, enabled=True),
|
||||
PeriodicTask(name='Somebody to Love', task='somebody_to_love',
|
||||
interval=self.interval, enabled=False),
|
||||
PeriodicTask(name='Tie Your Mother Down',
|
||||
task='tie_your_mother_down',
|
||||
interval=self.interval, enabled=False),
|
||||
PeriodicTask(name='Under Pressure', task='under_pressure',
|
||||
interval=self.interval, enabled=True),
|
||||
])
|
||||
names = [b.name for b in self.pt_admin.get_queryset(request)]
|
||||
self.assertListEqual(['Bohemian Rhapsody', 'Under Pressure',
|
||||
'Somebody to Love', 'Tie Your Mother Down'],
|
||||
names)
|
||||
|
||||
def test_enable_tasks_should_enable_disabled_periodic_tasks(self):
|
||||
"""
|
||||
enable_tasks action should enable selected periodic tasks
|
||||
"""
|
||||
PeriodicTask.objects.create(name='Killer Queen', task='killer_queen',
|
||||
interval=self.interval, enabled=False),
|
||||
queryset = PeriodicTask.objects.filter(pk=1)
|
||||
last_update = PeriodicTasks.objects.get(ident=1).last_update
|
||||
self.pt_admin.enable_tasks(request, queryset)
|
||||
new_last_update = PeriodicTasks.objects.get(ident=1).last_update
|
||||
self.assertTrue(PeriodicTask.objects.get(pk=1).enabled)
|
||||
self.assertNotEqual(last_update, new_last_update)
|
||||
|
||||
def test_disable_tasks_should_disable_enabled_periodic_tasks(self):
|
||||
"""
|
||||
disable_tasks action should disable selected periodic tasks
|
||||
"""
|
||||
PeriodicTask.objects.create(name='Killer Queen', task='killer_queen',
|
||||
interval=self.interval, enabled=True),
|
||||
queryset = PeriodicTask.objects.filter(pk=1)
|
||||
self.pt_admin.disable_tasks(request, queryset)
|
||||
self.assertFalse(PeriodicTask.objects.get(pk=1).enabled)
|
||||
|
||||
def test_for_valid_search_fields(self):
|
||||
"""
|
||||
Valid search fields should be ('name', 'task')
|
||||
"""
|
||||
search_fields = self.pt_admin.search_fields
|
||||
self.assertEqual(search_fields, ('name', 'task'))
|
||||
|
||||
for fieldname in search_fields:
|
||||
query = '%s__icontains' % fieldname
|
||||
kwargs = {query: 'Queen'}
|
||||
# We have no content, so the number of results if we search on
|
||||
# something should be zero.
|
||||
self.assertEquals(PeriodicTask.objects.filter(**kwargs).count(), 0)
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from billiard.einfo import ExceptionInfo
|
||||
|
||||
from celery import result
|
||||
from celery import states
|
||||
from celery.utils import gen_unique_id
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.backends.cache import CacheBackend
|
||||
from djcelery.tests.utils import unittest
|
||||
|
||||
|
||||
class SomeClass(object):
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
|
||||
class test_CacheBackend(unittest.TestCase):
|
||||
|
||||
def test_mark_as_done(self):
|
||||
cb = CacheBackend(app=app)
|
||||
|
||||
tid = gen_unique_id()
|
||||
|
||||
self.assertEqual(cb.get_status(tid), states.PENDING)
|
||||
self.assertIsNone(cb.get_result(tid))
|
||||
|
||||
cb.mark_as_done(tid, 42)
|
||||
self.assertEqual(cb.get_status(tid), states.SUCCESS)
|
||||
self.assertEqual(cb.get_result(tid), 42)
|
||||
self.assertTrue(cb.get_result(tid), 42)
|
||||
|
||||
def test_forget(self):
|
||||
b = CacheBackend(app=app)
|
||||
tid = gen_unique_id()
|
||||
b.mark_as_done(tid, {'foo': 'bar'})
|
||||
self.assertEqual(b.get_result(tid).get('foo'), 'bar')
|
||||
b.forget(tid)
|
||||
self.assertNotIn(tid, b._cache)
|
||||
self.assertIsNone(b.get_result(tid))
|
||||
|
||||
def test_save_restore_delete_group(self):
|
||||
backend = CacheBackend(app=app)
|
||||
group_id = gen_unique_id()
|
||||
subtask_ids = [gen_unique_id() for i in range(10)]
|
||||
subtasks = list(map(result.AsyncResult, subtask_ids))
|
||||
res = result.GroupResult(group_id, subtasks)
|
||||
res.save(backend=backend)
|
||||
saved = result.GroupResult.restore(group_id, backend=backend)
|
||||
self.assertListEqual(saved.subtasks, subtasks)
|
||||
self.assertEqual(saved.id, group_id)
|
||||
saved.delete(backend=backend)
|
||||
self.assertIsNone(result.GroupResult.restore(group_id,
|
||||
backend=backend))
|
||||
|
||||
def test_is_pickled(self):
|
||||
cb = CacheBackend(app=app)
|
||||
|
||||
tid2 = gen_unique_id()
|
||||
result = {'foo': 'baz', 'bar': SomeClass(12345)}
|
||||
cb.mark_as_done(tid2, result)
|
||||
# is serialized properly.
|
||||
rindb = cb.get_result(tid2)
|
||||
self.assertEqual(rindb.get('foo'), 'baz')
|
||||
self.assertEqual(rindb.get('bar').data, 12345)
|
||||
|
||||
def test_mark_as_failure(self):
|
||||
cb = CacheBackend(app=app)
|
||||
|
||||
einfo = None
|
||||
tid3 = gen_unique_id()
|
||||
try:
|
||||
raise KeyError('foo')
|
||||
except KeyError as exception:
|
||||
einfo = ExceptionInfo(sys.exc_info())
|
||||
cb.mark_as_failure(tid3, exception, traceback=einfo.traceback)
|
||||
self.assertEqual(cb.get_status(tid3), states.FAILURE)
|
||||
self.assertIsInstance(cb.get_result(tid3), KeyError)
|
||||
self.assertEqual(cb.get_traceback(tid3), einfo.traceback)
|
||||
|
||||
def test_process_cleanup(self):
|
||||
cb = CacheBackend(app=app)
|
||||
cb.process_cleanup()
|
||||
|
||||
def test_set_expires(self):
|
||||
cb1 = CacheBackend(app=app, expires=timedelta(seconds=16))
|
||||
self.assertEqual(cb1.expires, 16)
|
||||
cb2 = CacheBackend(app=app, expires=32)
|
||||
self.assertEqual(cb2.expires, 32)
|
||||
|
||||
|
||||
class test_custom_CacheBackend(unittest.TestCase):
|
||||
|
||||
def test_custom_cache_backend(self):
|
||||
from celery import current_app
|
||||
prev_backend = current_app.conf.CELERY_CACHE_BACKEND
|
||||
prev_module = sys.modules['djcelery.backends.cache']
|
||||
|
||||
current_app.conf.CELERY_CACHE_BACKEND = 'dummy'
|
||||
sys.modules.pop('djcelery.backends.cache')
|
||||
try:
|
||||
from djcelery.backends.cache import cache
|
||||
from django.core.cache import cache as django_cache
|
||||
self.assertEqual(cache.__class__.__module__,
|
||||
'django.core.cache.backends.dummy')
|
||||
self.assertIsNot(cache, django_cache)
|
||||
finally:
|
||||
current_app.conf.CELERY_CACHE_BACKEND = prev_backend
|
||||
sys.modules['djcelery.backends.cache'] = prev_module
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import celery
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from celery import current_app
|
||||
from celery import states
|
||||
from celery.result import AsyncResult
|
||||
from celery.task import PeriodicTask
|
||||
from celery.utils import gen_unique_id
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.backends.database import DatabaseBackend
|
||||
from djcelery.utils import now
|
||||
from djcelery.tests.utils import unittest
|
||||
|
||||
|
||||
class SomeClass(object):
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
|
||||
class MyPeriodicTask(PeriodicTask):
|
||||
name = 'c.u.my-periodic-task-244'
|
||||
run_every = timedelta(seconds=1)
|
||||
|
||||
def run(self, **kwargs):
|
||||
return 42
|
||||
|
||||
|
||||
class TestDatabaseBackend(unittest.TestCase):
|
||||
|
||||
def test_backend(self):
|
||||
b = DatabaseBackend(app=app)
|
||||
tid = gen_unique_id()
|
||||
|
||||
self.assertEqual(b.get_status(tid), states.PENDING)
|
||||
self.assertIsNone(b.get_result(tid))
|
||||
|
||||
b.mark_as_done(tid, 42)
|
||||
self.assertEqual(b.get_status(tid), states.SUCCESS)
|
||||
self.assertEqual(b.get_result(tid), 42)
|
||||
|
||||
tid2 = gen_unique_id()
|
||||
result = {'foo': 'baz', 'bar': SomeClass(12345)}
|
||||
b.mark_as_done(tid2, result)
|
||||
# is serialized properly.
|
||||
rindb = b.get_result(tid2)
|
||||
self.assertEqual(rindb.get('foo'), 'baz')
|
||||
self.assertEqual(rindb.get('bar').data, 12345)
|
||||
|
||||
tid3 = gen_unique_id()
|
||||
try:
|
||||
raise KeyError('foo')
|
||||
except KeyError as exception:
|
||||
b.mark_as_failure(tid3, exception)
|
||||
|
||||
self.assertEqual(b.get_status(tid3), states.FAILURE)
|
||||
self.assertIsInstance(b.get_result(tid3), KeyError)
|
||||
|
||||
def test_forget(self):
|
||||
b = DatabaseBackend(app=app)
|
||||
tid = gen_unique_id()
|
||||
b.mark_as_done(tid, {'foo': 'bar'})
|
||||
x = AsyncResult(tid)
|
||||
self.assertEqual(x.result.get('foo'), 'bar')
|
||||
x.forget()
|
||||
if celery.VERSION[0:3] == (3, 1, 10):
|
||||
# bug in 3.1.10 means result did not clear cache after forget.
|
||||
x._cache = None
|
||||
self.assertIsNone(x.result)
|
||||
|
||||
def test_group_store(self):
|
||||
b = DatabaseBackend(app=app)
|
||||
tid = gen_unique_id()
|
||||
|
||||
self.assertIsNone(b.restore_group(tid))
|
||||
|
||||
result = {'foo': 'baz', 'bar': SomeClass(12345)}
|
||||
b.save_group(tid, result)
|
||||
rindb = b.restore_group(tid)
|
||||
self.assertIsNotNone(rindb)
|
||||
self.assertEqual(rindb.get('foo'), 'baz')
|
||||
self.assertEqual(rindb.get('bar').data, 12345)
|
||||
b.delete_group(tid)
|
||||
self.assertIsNone(b.restore_group(tid))
|
||||
|
||||
def test_cleanup(self):
|
||||
b = DatabaseBackend(app=app)
|
||||
b.TaskModel._default_manager.all().delete()
|
||||
ids = [gen_unique_id() for _ in range(3)]
|
||||
for i, res in enumerate((16, 32, 64)):
|
||||
b.mark_as_done(ids[i], res)
|
||||
|
||||
self.assertEqual(b.TaskModel._default_manager.count(), 3)
|
||||
|
||||
then = now() - current_app.conf.CELERY_TASK_RESULT_EXPIRES * 2
|
||||
# Have to avoid save() because it applies the auto_now=True.
|
||||
b.TaskModel._default_manager.filter(task_id__in=ids[:-1]) \
|
||||
.update(date_done=then)
|
||||
|
||||
b.cleanup()
|
||||
self.assertEqual(b.TaskModel._default_manager.count(), 1)
|
||||
@@ -0,0 +1,32 @@
|
||||
# coding: utf-8
|
||||
from django import VERSION
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
from ._compat import patch
|
||||
|
||||
|
||||
CELERYD_COMMAND = 'djcelery.management.commands.celeryd.Command.handle'
|
||||
|
||||
|
||||
def test_celeryd_command():
|
||||
if VERSION >= (1, 10):
|
||||
traceback = False
|
||||
else:
|
||||
traceback = None
|
||||
with patch(CELERYD_COMMAND) as handle:
|
||||
execute_from_command_line(['manage.py', 'celeryd', '--hostname=test',
|
||||
'--loglevel=info'])
|
||||
handle.assert_called_with(
|
||||
autoreload=None, autoscale=None, beat=None, broker=None,
|
||||
concurrency=0, detach=None, exclude_queues=[], executable=None,
|
||||
gid=None, heartbeat_interval=None, hostname="test", include=[],
|
||||
logfile=None, loglevel='info', max_tasks_per_child=None,
|
||||
no_color=False, no_execv=False, optimization=None, pidfile=None,
|
||||
pool_cls='prefork', purge=False, pythonpath=None, queues=[],
|
||||
quiet=None, schedule_filename='celerybeat-schedule',
|
||||
scheduler_cls=None, send_events=False, settings=None,
|
||||
state_db=None, task_soft_time_limit=None,
|
||||
task_time_limit=None, traceback=traceback, uid=None, umask=None,
|
||||
verbosity=1, without_gossip=False, without_heartbeat=False,
|
||||
without_mingle=False, working_directory=None
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from celery.registry import tasks
|
||||
|
||||
from djcelery.loaders import autodiscover
|
||||
from djcelery.tests.utils import unittest
|
||||
|
||||
|
||||
class TestDiscovery(unittest.TestCase):
|
||||
|
||||
def assertDiscovery(self):
|
||||
apps = autodiscover()
|
||||
self.assertTrue(apps)
|
||||
self.assertIn('c.unittest.SomeAppTask', tasks)
|
||||
self.assertEqual(tasks['c.unittest.SomeAppTask'].run(), 42)
|
||||
|
||||
def test_discovery(self):
|
||||
if 'someapp' in settings.INSTALLED_APPS:
|
||||
self.assertDiscovery()
|
||||
|
||||
def test_discovery_with_broken(self):
|
||||
warnings.resetwarnings()
|
||||
if 'someapp' in settings.INSTALLED_APPS:
|
||||
installed_apps = list(settings.INSTALLED_APPS)
|
||||
settings.INSTALLED_APPS = installed_apps + ['xxxnot.aexist']
|
||||
try:
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
autodiscover()
|
||||
self.assertTrue(log)
|
||||
finally:
|
||||
settings.INSTALLED_APPS = installed_apps
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery import loaders
|
||||
|
||||
from djcelery import loaders as djloaders
|
||||
from djcelery.app import app
|
||||
from djcelery.tests.utils import unittest
|
||||
|
||||
|
||||
class TestDjangoLoader(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.loader = djloaders.DjangoLoader(app=app)
|
||||
|
||||
def test_get_loader_cls(self):
|
||||
|
||||
self.assertEqual(loaders.get_loader_cls('django'),
|
||||
self.loader.__class__)
|
||||
# Execute cached branch.
|
||||
self.assertEqual(loaders.get_loader_cls('django'),
|
||||
self.loader.__class__)
|
||||
|
||||
def test_on_worker_init(self):
|
||||
from django.conf import settings
|
||||
old_imports = getattr(settings, 'CELERY_IMPORTS', ())
|
||||
settings.CELERY_IMPORTS = ('xxx.does.not.exist', )
|
||||
try:
|
||||
self.assertRaises(ImportError, self.loader.import_default_modules)
|
||||
finally:
|
||||
settings.CELERY_IMPORTS = old_imports
|
||||
|
||||
def test_race_protection(self):
|
||||
djloaders._RACE_PROTECTION = True
|
||||
try:
|
||||
self.assertFalse(self.loader.on_worker_init())
|
||||
finally:
|
||||
djloaders._RACE_PROTECTION = False
|
||||
|
||||
def test_find_related_module_no_path(self):
|
||||
self.assertFalse(djloaders.find_related_module('sys', 'tasks'))
|
||||
|
||||
def test_find_related_module_no_related(self):
|
||||
self.assertFalse(
|
||||
djloaders.find_related_module('someapp', 'frobulators'),
|
||||
)
|
||||
@@ -0,0 +1,102 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from celery import states
|
||||
from celery.utils import gen_unique_id
|
||||
|
||||
from djcelery import celery
|
||||
from djcelery.models import TaskMeta, TaskSetMeta
|
||||
from djcelery.utils import now
|
||||
from djcelery.tests.utils import unittest
|
||||
from djcelery.compat import unicode
|
||||
|
||||
|
||||
class TestModels(unittest.TestCase):
|
||||
|
||||
def createTaskMeta(self):
|
||||
id = gen_unique_id()
|
||||
taskmeta, created = TaskMeta.objects.get_or_create(task_id=id)
|
||||
return taskmeta
|
||||
|
||||
def createTaskSetMeta(self):
|
||||
id = gen_unique_id()
|
||||
tasksetmeta, created = TaskSetMeta.objects.get_or_create(taskset_id=id)
|
||||
return tasksetmeta
|
||||
|
||||
def test_taskmeta(self):
|
||||
m1 = self.createTaskMeta()
|
||||
m2 = self.createTaskMeta()
|
||||
m3 = self.createTaskMeta()
|
||||
self.assertTrue(unicode(m1).startswith('<Task:'))
|
||||
self.assertTrue(m1.task_id)
|
||||
self.assertIsInstance(m1.date_done, datetime)
|
||||
|
||||
self.assertEqual(
|
||||
TaskMeta.objects.get_task(m1.task_id).task_id,
|
||||
m1.task_id,
|
||||
)
|
||||
self.assertNotEqual(TaskMeta.objects.get_task(m1.task_id).status,
|
||||
states.SUCCESS)
|
||||
TaskMeta.objects.store_result(m1.task_id, True, status=states.SUCCESS)
|
||||
TaskMeta.objects.store_result(m2.task_id, True, status=states.SUCCESS)
|
||||
self.assertEqual(TaskMeta.objects.get_task(m1.task_id).status,
|
||||
states.SUCCESS)
|
||||
self.assertEqual(TaskMeta.objects.get_task(m2.task_id).status,
|
||||
states.SUCCESS)
|
||||
|
||||
# Have to avoid save() because it applies the auto_now=True.
|
||||
TaskMeta.objects.filter(
|
||||
task_id=m1.task_id
|
||||
).update(date_done=now() - timedelta(days=10))
|
||||
|
||||
expired = TaskMeta.objects.get_all_expired(
|
||||
celery.conf.CELERY_TASK_RESULT_EXPIRES,
|
||||
)
|
||||
self.assertIn(m1, expired)
|
||||
self.assertNotIn(m2, expired)
|
||||
self.assertNotIn(m3, expired)
|
||||
|
||||
TaskMeta.objects.delete_expired(
|
||||
celery.conf.CELERY_TASK_RESULT_EXPIRES,
|
||||
)
|
||||
self.assertNotIn(m1, TaskMeta.objects.all())
|
||||
|
||||
def test_tasksetmeta(self):
|
||||
m1 = self.createTaskSetMeta()
|
||||
m2 = self.createTaskSetMeta()
|
||||
m3 = self.createTaskSetMeta()
|
||||
self.assertTrue(unicode(m1).startswith('<TaskSet:'))
|
||||
self.assertTrue(m1.taskset_id)
|
||||
self.assertIsInstance(m1.date_done, datetime)
|
||||
|
||||
self.assertEqual(
|
||||
TaskSetMeta.objects.restore_taskset(m1.taskset_id).taskset_id,
|
||||
m1.taskset_id,
|
||||
)
|
||||
|
||||
# Have to avoid save() because it applies the auto_now=True.
|
||||
TaskSetMeta.objects.filter(
|
||||
taskset_id=m1.taskset_id,
|
||||
).update(date_done=now() - timedelta(days=10))
|
||||
|
||||
expired = TaskSetMeta.objects.get_all_expired(
|
||||
celery.conf.CELERY_TASK_RESULT_EXPIRES,
|
||||
)
|
||||
self.assertIn(m1, expired)
|
||||
self.assertNotIn(m2, expired)
|
||||
self.assertNotIn(m3, expired)
|
||||
|
||||
TaskSetMeta.objects.delete_expired(
|
||||
celery.conf.CELERY_TASK_RESULT_EXPIRES,
|
||||
)
|
||||
self.assertNotIn(m1, TaskSetMeta.objects.all())
|
||||
|
||||
m4 = self.createTaskSetMeta()
|
||||
self.assertEqual(
|
||||
TaskSetMeta.objects.restore_taskset(m4.taskset_id).taskset_id,
|
||||
m4.taskset_id,
|
||||
)
|
||||
|
||||
TaskSetMeta.objects.delete_taskset(m4.taskset_id)
|
||||
self.assertIsNone(TaskSetMeta.objects.restore_taskset(m4.taskset_id))
|
||||
@@ -0,0 +1,336 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import count
|
||||
|
||||
from celery.five import monotonic
|
||||
from celery.schedules import schedule, crontab
|
||||
|
||||
from djcelery import schedulers
|
||||
from djcelery import celery
|
||||
from djcelery.app import app
|
||||
from djcelery.models import PeriodicTask, IntervalSchedule, CrontabSchedule
|
||||
from djcelery.models import PeriodicTasks
|
||||
from djcelery.tests.utils import unittest
|
||||
from djcelery.compat import unicode
|
||||
|
||||
|
||||
def create_model_interval(schedule, **kwargs):
|
||||
interval = IntervalSchedule.from_schedule(schedule)
|
||||
interval.save()
|
||||
return create_model(interval=interval, **kwargs)
|
||||
|
||||
|
||||
def create_model_crontab(schedule, **kwargs):
|
||||
crontab = CrontabSchedule.from_schedule(schedule)
|
||||
crontab.save()
|
||||
return create_model(crontab=crontab, **kwargs)
|
||||
|
||||
|
||||
_ids = count(0)
|
||||
|
||||
|
||||
def create_model(Model=PeriodicTask, **kwargs):
|
||||
entry = dict(name='thefoo{0}'.format(next(_ids)),
|
||||
task='djcelery.unittest.add{0}'.format(next(_ids)),
|
||||
args='[2, 2]',
|
||||
kwargs='{"callback": "foo"}',
|
||||
queue='xaz',
|
||||
routing_key='cpu',
|
||||
exchange='foo')
|
||||
return Model(**dict(entry, **kwargs))
|
||||
|
||||
|
||||
class EntryTrackSave(schedulers.ModelEntry):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.saved = 0
|
||||
super(EntryTrackSave, self).__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
self.saved += 1
|
||||
super(EntryTrackSave, self).save()
|
||||
|
||||
|
||||
class EntrySaveRaises(schedulers.ModelEntry):
|
||||
|
||||
def save(self):
|
||||
raise RuntimeError('this is expected')
|
||||
|
||||
|
||||
class TrackingScheduler(schedulers.DatabaseScheduler):
|
||||
Entry = EntryTrackSave
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.flushed = 0
|
||||
schedulers.DatabaseScheduler.__init__(self, *args, **kwargs)
|
||||
|
||||
def sync(self):
|
||||
self.flushed += 1
|
||||
schedulers.DatabaseScheduler.sync(self)
|
||||
|
||||
|
||||
class test_ModelEntry(unittest.TestCase):
|
||||
Entry = EntryTrackSave
|
||||
|
||||
def tearDown(self):
|
||||
PeriodicTask.objects.all().delete()
|
||||
|
||||
def test_entry(self):
|
||||
m = create_model_interval(schedule(timedelta(seconds=10)))
|
||||
e = self.Entry(m)
|
||||
|
||||
self.assertListEqual(e.args, [2, 2])
|
||||
self.assertDictEqual(e.kwargs, {'callback': 'foo'})
|
||||
self.assertTrue(e.schedule)
|
||||
self.assertEqual(e.total_run_count, 0)
|
||||
self.assertIsInstance(e.last_run_at, datetime)
|
||||
self.assertEqual(e.options.get('queue'), 'xaz')
|
||||
self.assertEqual(e.options.get('exchange'), 'foo')
|
||||
self.assertEqual(e.options.get('routing_key'), 'cpu')
|
||||
|
||||
right_now = celery.now()
|
||||
m2 = create_model_interval(schedule(timedelta(seconds=10)),
|
||||
last_run_at=right_now)
|
||||
self.assertTrue(m2.last_run_at)
|
||||
e2 = self.Entry(m2)
|
||||
self.assertIs(e2.last_run_at, right_now)
|
||||
|
||||
e3 = e2.next()
|
||||
self.assertGreater(e3.last_run_at, e2.last_run_at)
|
||||
self.assertEqual(e3.total_run_count, 1)
|
||||
|
||||
def test_from_entry(self):
|
||||
name = 'interval-vs-crontab'
|
||||
entry = {'task': 'djcelery.unittest.add{0}'.format(next(_ids)),
|
||||
'args': '[2, 2]',
|
||||
'schedule': timedelta(hours=24), }
|
||||
self.Entry.from_entry(name, **entry)
|
||||
schedule1 = PeriodicTask.objects.get(name=name).schedule
|
||||
self.assertIsInstance(schedule1, schedule)
|
||||
|
||||
# update schedule
|
||||
entry['schedule'] = crontab(minute=0, hour='*/6')
|
||||
self.Entry.from_entry(name, **entry)
|
||||
schedule2 = PeriodicTask.objects.get(name=name).schedule
|
||||
self.assertIsInstance(schedule2, crontab)
|
||||
|
||||
|
||||
class test_DatabaseScheduler(unittest.TestCase):
|
||||
Scheduler = TrackingScheduler
|
||||
|
||||
def setUp(self):
|
||||
PeriodicTask.objects.all().delete()
|
||||
self.prev_schedule = celery.conf.CELERYBEAT_SCHEDULE
|
||||
celery.conf.CELERYBEAT_SCHEDULE = {}
|
||||
m1 = create_model_interval(schedule(timedelta(seconds=10)))
|
||||
m2 = create_model_interval(schedule(timedelta(minutes=20)))
|
||||
m3 = create_model_crontab(crontab(minute='2,4,5'))
|
||||
for obj in m1, m2, m3:
|
||||
obj.save()
|
||||
self.s = self.Scheduler(app=app)
|
||||
self.m1 = PeriodicTask.objects.get(name=m1.name)
|
||||
self.m2 = PeriodicTask.objects.get(name=m2.name)
|
||||
self.m3 = PeriodicTask.objects.get(name=m3.name)
|
||||
|
||||
def tearDown(self):
|
||||
celery.conf.CELERYBEAT_SCHEDULE = self.prev_schedule
|
||||
PeriodicTask.objects.all().delete()
|
||||
|
||||
def test_constructor(self):
|
||||
self.assertIsInstance(self.s._dirty, set)
|
||||
self.assertIsNone(self.s._last_sync)
|
||||
self.assertTrue(self.s.sync_every)
|
||||
|
||||
def test_all_as_schedule(self):
|
||||
sched = self.s.schedule
|
||||
self.assertTrue(sched)
|
||||
self.assertEqual(len(sched), 4)
|
||||
self.assertIn('celery.backend_cleanup', sched)
|
||||
for n, e in sched.items():
|
||||
self.assertIsInstance(e, self.s.Entry)
|
||||
|
||||
def test_schedule_changed(self):
|
||||
self.m2.args = '[16, 16]'
|
||||
self.m2.save()
|
||||
e2 = self.s.schedule[self.m2.name]
|
||||
self.assertListEqual(e2.args, [16, 16])
|
||||
|
||||
self.m1.args = '[32, 32]'
|
||||
self.m1.save()
|
||||
e1 = self.s.schedule[self.m1.name]
|
||||
self.assertListEqual(e1.args, [32, 32])
|
||||
|
||||
self.m3.delete()
|
||||
self.assertRaises(KeyError, self.s.schedule.__getitem__, self.m3.name)
|
||||
|
||||
def test_should_sync(self):
|
||||
self.assertTrue(self.s.should_sync())
|
||||
self.s._last_sync = monotonic()
|
||||
self.s._tasks_since_sync = 0
|
||||
self.assertFalse(self.s.should_sync())
|
||||
self.s._last_sync -= self.s.sync_every + 1
|
||||
self.assertTrue(self.s.should_sync())
|
||||
|
||||
def test_reserve(self):
|
||||
e1 = self.s.schedule[self.m1.name]
|
||||
self.s.schedule[self.m1.name] = self.s.reserve(e1)
|
||||
self.assertEqual(self.s.flushed, 1)
|
||||
|
||||
e2 = self.s.schedule[self.m2.name]
|
||||
self.s.schedule[self.m2.name] = self.s.reserve(e2)
|
||||
self.assertEqual(self.s.flushed, 1)
|
||||
self.assertIn(self.m2.name, self.s._dirty)
|
||||
|
||||
def test_sync_saves_last_run_at(self):
|
||||
e1 = self.s.schedule[self.m2.name]
|
||||
last_run = e1.last_run_at
|
||||
last_run2 = last_run - timedelta(days=1)
|
||||
e1.model.last_run_at = last_run2
|
||||
self.s._dirty.add(self.m2.name)
|
||||
self.s.sync()
|
||||
|
||||
e2 = self.s.schedule[self.m2.name]
|
||||
self.assertEqual(e2.last_run_at, last_run2)
|
||||
|
||||
def test_sync_syncs_before_save(self):
|
||||
# Get the entry for m2
|
||||
e1 = self.s.schedule[self.m2.name]
|
||||
|
||||
# Increment the entry (but make sure it doesn't sync)
|
||||
self.s._last_sync = monotonic()
|
||||
e2 = self.s.schedule[e1.name] = self.s.reserve(e1)
|
||||
self.assertEqual(self.s.flushed, 1)
|
||||
|
||||
# Fetch the raw object from db, change the args
|
||||
# and save the changes.
|
||||
m2 = PeriodicTask.objects.get(pk=self.m2.pk)
|
||||
m2.args = '[16, 16]'
|
||||
m2.save()
|
||||
|
||||
# get_schedule should now see the schedule has changed.
|
||||
# and also sync the dirty objects.
|
||||
e3 = self.s.schedule[self.m2.name]
|
||||
self.assertEqual(self.s.flushed, 2)
|
||||
self.assertEqual(e3.last_run_at, e2.last_run_at)
|
||||
self.assertListEqual(e3.args, [16, 16])
|
||||
|
||||
def test_sync_not_dirty(self):
|
||||
self.s._dirty.clear()
|
||||
self.s.sync()
|
||||
|
||||
def test_sync_object_gone(self):
|
||||
self.s._dirty.add('does-not-exist')
|
||||
self.s.sync()
|
||||
|
||||
def test_sync_rollback_on_save_error(self):
|
||||
self.s.schedule[self.m1.name] = EntrySaveRaises(self.m1)
|
||||
self.s._dirty.add(self.m1.name)
|
||||
self.assertRaises(RuntimeError, self.s.sync)
|
||||
|
||||
def test_dynamic_create_update_delete_task(self):
|
||||
schedule_dict = {
|
||||
'task': 'some_task',
|
||||
'schedule': timedelta(seconds=5),
|
||||
'args': ('arg1', 'arg2'),
|
||||
}
|
||||
task1_name = 'test_task 1'
|
||||
task2_name = 'test_task 2'
|
||||
schedulers.DatabaseScheduler.create_or_update_task(task1_name,
|
||||
**schedule_dict)
|
||||
schedulers.DatabaseScheduler.create_or_update_task(task2_name,
|
||||
**schedule_dict)
|
||||
PeriodicTask.objects.get(name=task1_name) # assert not raises
|
||||
PeriodicTask.objects.get(name=task2_name)
|
||||
schedulers.DatabaseScheduler.create_or_update_task(task1_name,
|
||||
args=('arg3',))
|
||||
self.assertEqual(PeriodicTask.objects.get(name=task1_name).args,
|
||||
u'["arg3"]')
|
||||
schedulers.DatabaseScheduler.create_or_update_task(
|
||||
task2_name, schedule=timedelta(10))
|
||||
self.assertEqual(PeriodicTask.objects.get(name=task2_name).schedule,
|
||||
timedelta(10))
|
||||
schedulers.DatabaseScheduler.delete_task(task1_name)
|
||||
schedulers.DatabaseScheduler.delete_task(task2_name)
|
||||
self.assertRaises(PeriodicTask.DoesNotExist,
|
||||
lambda: PeriodicTask.objects.get(name=task1_name))
|
||||
self.assertRaises(PeriodicTask.DoesNotExist,
|
||||
lambda: PeriodicTask.objects.get(name=task2_name))
|
||||
|
||||
|
||||
class test_models(unittest.TestCase):
|
||||
|
||||
def test_IntervalSchedule_unicode(self):
|
||||
self.assertEqual(unicode(IntervalSchedule(every=1, period='seconds')),
|
||||
'every second')
|
||||
self.assertEqual(unicode(IntervalSchedule(every=10, period='seconds')),
|
||||
'every 10 seconds')
|
||||
|
||||
def test_CrontabSchedule_unicode(self):
|
||||
self.assertEqual(unicode(CrontabSchedule(minute=3,
|
||||
hour=3,
|
||||
day_of_week=None)),
|
||||
'3 3 * * * (m/h/d/dM/MY)')
|
||||
self.assertEqual(unicode(CrontabSchedule(minute=3,
|
||||
hour=3,
|
||||
day_of_week='tue',
|
||||
day_of_month='*/2',
|
||||
month_of_year='4,6')),
|
||||
'3 3 tue */2 4,6 (m/h/d/dM/MY)')
|
||||
|
||||
def test_PeriodicTask_unicode_interval(self):
|
||||
p = create_model_interval(schedule(timedelta(seconds=10)))
|
||||
self.assertEqual(unicode(p),
|
||||
'{0}: every 10 seconds'.format(p.name))
|
||||
|
||||
def test_PeriodicTask_unicode_crontab(self):
|
||||
p = create_model_crontab(crontab(hour='4, 5', day_of_week='4, 5'))
|
||||
self.assertEqual(unicode(p),
|
||||
'{0}: * 4,5 4,5 * * (m/h/d/dM/MY)'.format(p.name))
|
||||
|
||||
def test_PeriodicTask_schedule_property(self):
|
||||
p1 = create_model_interval(schedule(timedelta(seconds=10)))
|
||||
s1 = p1.schedule
|
||||
self.assertEqual(s1.run_every.total_seconds(), 10)
|
||||
|
||||
p2 = create_model_crontab(crontab(hour='4, 5',
|
||||
minute='10,20,30',
|
||||
day_of_month='1-7',
|
||||
month_of_year='*/3'))
|
||||
s2 = p2.schedule
|
||||
self.assertSetEqual(s2.hour, set([4, 5]))
|
||||
self.assertSetEqual(s2.minute, set([10, 20, 30]))
|
||||
self.assertSetEqual(s2.day_of_week, set([0, 1, 2, 3, 4, 5, 6]))
|
||||
self.assertSetEqual(s2.day_of_month, set([1, 2, 3, 4, 5, 6, 7]))
|
||||
self.assertSetEqual(s2.month_of_year, set([1, 4, 7, 10]))
|
||||
|
||||
def test_PeriodicTask_unicode_no_schedule(self):
|
||||
p = create_model()
|
||||
self.assertEqual(unicode(p), '{0}: {{no schedule}}'.format(p.name))
|
||||
|
||||
def test_CrontabSchedule_schedule(self):
|
||||
s = CrontabSchedule(minute='3, 7', hour='3, 4', day_of_week='*',
|
||||
day_of_month='1, 16', month_of_year='1, 7')
|
||||
self.assertEqual(s.schedule.minute, set([3, 7]))
|
||||
self.assertEqual(s.schedule.hour, set([3, 4]))
|
||||
self.assertEqual(s.schedule.day_of_week, set([0, 1, 2, 3, 4, 5, 6]))
|
||||
self.assertEqual(s.schedule.day_of_month, set([1, 16]))
|
||||
self.assertEqual(s.schedule.month_of_year, set([1, 7]))
|
||||
|
||||
|
||||
class test_model_PeriodicTasks(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
PeriodicTasks.objects.all().delete()
|
||||
|
||||
def test_track_changes(self):
|
||||
self.assertIsNone(PeriodicTasks.last_change())
|
||||
m1 = create_model_interval(schedule(timedelta(seconds=10)))
|
||||
m1.save()
|
||||
x = PeriodicTasks.last_change()
|
||||
self.assertTrue(x)
|
||||
m1.args = '(23, 24)'
|
||||
m1.save()
|
||||
y = PeriodicTasks.last_change()
|
||||
self.assertTrue(y)
|
||||
self.assertGreater(y, x)
|
||||
@@ -0,0 +1,250 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
from itertools import count
|
||||
from time import time
|
||||
|
||||
from celery import states
|
||||
from celery.events import Event as _Event
|
||||
from celery.events.state import State, Worker, Task
|
||||
from celery.utils import gen_unique_id
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from djcelery import celery
|
||||
from djcelery import snapshot
|
||||
from djcelery import models
|
||||
from djcelery.utils import make_aware
|
||||
from djcelery.tests.utils import unittest
|
||||
from djcelery.compat import unicode
|
||||
|
||||
|
||||
_ids = count(0)
|
||||
_clock = count(1)
|
||||
|
||||
|
||||
def Event(*args, **kwargs):
|
||||
kwargs.setdefault('clock', next(_clock))
|
||||
kwargs.setdefault('local_received', time())
|
||||
return _Event(*args, **kwargs)
|
||||
|
||||
|
||||
def create_task(worker, **kwargs):
|
||||
d = dict(uuid=gen_unique_id(),
|
||||
name='djcelery.unittest.task{0}'.format(next(_ids)),
|
||||
worker=worker)
|
||||
return Task(**dict(d, **kwargs))
|
||||
|
||||
|
||||
class test_Camera(unittest.TestCase):
|
||||
Camera = snapshot.Camera
|
||||
|
||||
def setUp(self):
|
||||
self.state = State()
|
||||
self.cam = self.Camera(self.state)
|
||||
|
||||
def test_constructor(self):
|
||||
cam = self.Camera(State())
|
||||
self.assertTrue(cam.state)
|
||||
self.assertTrue(cam.freq)
|
||||
self.assertTrue(cam.cleanup_freq)
|
||||
self.assertTrue(cam.logger)
|
||||
|
||||
def test_get_heartbeat(self):
|
||||
worker = Worker(hostname='fuzzie')
|
||||
self.assertIsNone(self.cam.get_heartbeat(worker))
|
||||
t1 = time()
|
||||
t2 = time()
|
||||
t3 = time()
|
||||
for t in t1, t2, t3:
|
||||
worker.event('heartbeat', t, t, {})
|
||||
self.state.workers[worker.hostname] = worker
|
||||
self.assertEqual(self.cam.get_heartbeat(worker),
|
||||
make_aware(datetime.fromtimestamp(t3)))
|
||||
|
||||
def test_handle_worker(self):
|
||||
worker = Worker(hostname='fuzzie')
|
||||
worker.event('online', time(), time(), {})
|
||||
self.cam._last_worker_write.clear()
|
||||
m = self.cam.handle_worker((worker.hostname, worker))
|
||||
self.assertTrue(m)
|
||||
self.assertTrue(m.hostname)
|
||||
self.assertTrue(m.last_heartbeat)
|
||||
self.assertTrue(m.is_alive())
|
||||
self.assertEqual(unicode(m), unicode(m.hostname))
|
||||
self.assertTrue(repr(m))
|
||||
|
||||
def test_handle_task_received(self):
|
||||
worker = Worker(hostname='fuzzie')
|
||||
worker.event('online', time(), time(), {})
|
||||
self.cam.handle_worker((worker.hostname, worker))
|
||||
|
||||
task = create_task(worker)
|
||||
task.event('received', time(), time(), {})
|
||||
self.assertEqual(task.state, 'RECEIVED')
|
||||
mt = self.cam.handle_task((task.uuid, task))
|
||||
self.assertEqual(mt.name, task.name)
|
||||
self.assertTrue(unicode(mt))
|
||||
self.assertTrue(repr(mt))
|
||||
mt.eta = celery.now()
|
||||
self.assertIn('eta', unicode(mt))
|
||||
self.assertIn(mt, models.TaskState.objects.active())
|
||||
|
||||
def test_handle_task(self):
|
||||
worker1 = Worker(hostname='fuzzie')
|
||||
worker1.event('online', time(), time(), {})
|
||||
mw = self.cam.handle_worker((worker1.hostname, worker1))
|
||||
task1 = create_task(worker1)
|
||||
task1.event('received', time(), time(), {})
|
||||
mt = self.cam.handle_task((task1.uuid, task1))
|
||||
self.assertEqual(mt.worker, mw)
|
||||
|
||||
worker2 = Worker(hostname=None)
|
||||
task2 = create_task(worker2)
|
||||
task2.event('received', time(), time(), {})
|
||||
mt = self.cam.handle_task((task2.uuid, task2))
|
||||
self.assertIsNone(mt.worker)
|
||||
|
||||
task1.event('succeeded', time(), time(), {'result': 42})
|
||||
self.assertEqual(task1.state, states.SUCCESS)
|
||||
self.assertEqual(task1.result, 42)
|
||||
mt = self.cam.handle_task((task1.uuid, task1))
|
||||
self.assertEqual(mt.name, task1.name)
|
||||
self.assertEqual(mt.result, 42)
|
||||
|
||||
task3 = create_task(worker1, name=None)
|
||||
task3.event('revoked', time(), time(), {})
|
||||
mt = self.cam.handle_task((task3.uuid, task3))
|
||||
self.assertIsNone(mt)
|
||||
|
||||
def test_handle_task_timezone(self):
|
||||
worker = Worker(hostname='fuzzie')
|
||||
worker.event('online', time(), time(), {})
|
||||
self.cam.handle_worker((worker.hostname, worker))
|
||||
|
||||
tstamp = 1464793200.0 # 2016-06-01T15:00:00Z
|
||||
|
||||
with override_settings(USE_TZ=True, TIME_ZONE='Europe/Helsinki'):
|
||||
task = create_task(worker,
|
||||
eta='2016-06-01T15:16:17.654321+00:00',
|
||||
expires='2016-07-01T15:16:17.765432+03:00')
|
||||
task.event('received', tstamp, tstamp, {})
|
||||
mt = self.cam.handle_task((task.uuid, task))
|
||||
self.assertEqual(
|
||||
mt.tstamp,
|
||||
datetime(2016, 6, 1, 15, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
self.assertEqual(
|
||||
mt.eta,
|
||||
datetime(2016, 6, 1, 15, 16, 17, 654321, tzinfo=timezone.utc),
|
||||
)
|
||||
self.assertEqual(
|
||||
mt.expires,
|
||||
datetime(2016, 7, 1, 12, 16, 17, 765432, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
task = create_task(worker, eta='2016-06-04T15:16:17.654321')
|
||||
task.event('received', tstamp, tstamp, {})
|
||||
mt = self.cam.handle_task((task.uuid, task))
|
||||
self.assertEqual(
|
||||
mt.eta,
|
||||
datetime(2016, 6, 4, 15, 16, 17, 654321, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
with override_settings(USE_TZ=False, TIME_ZONE='Europe/Helsinki'):
|
||||
task = create_task(worker,
|
||||
eta='2016-06-01T15:16:17.654321+00:00',
|
||||
expires='2016-07-01T15:16:17.765432+03:00')
|
||||
task.event('received', tstamp, tstamp, {})
|
||||
mt = self.cam.handle_task((task.uuid, task))
|
||||
self.assertEqual(mt.tstamp, datetime(2016, 6, 1, 18, 0, 0))
|
||||
self.assertEqual(mt.eta, datetime(2016, 6, 1, 18, 16, 17, 654321))
|
||||
self.assertEqual(mt.expires,
|
||||
datetime(2016, 7, 1, 15, 16, 17, 765432))
|
||||
|
||||
task = create_task(worker, eta='2016-06-04T15:16:17.654321')
|
||||
task.event('received', tstamp, tstamp, {})
|
||||
mt = self.cam.handle_task((task.uuid, task))
|
||||
self.assertEqual(mt.eta, datetime(2016, 6, 4, 15, 16, 17, 654321))
|
||||
|
||||
def assertExpires(self, dec, expired, tasks=10):
|
||||
# Cleanup leftovers from previous tests
|
||||
self.cam.on_cleanup()
|
||||
|
||||
worker = Worker(hostname='fuzzie')
|
||||
worker.event('online', time(), time(), {})
|
||||
for total in range(tasks):
|
||||
task = create_task(worker)
|
||||
task.event('received', time() - dec, time() - dec, {})
|
||||
task.event('succeeded', time() - dec, time() - dec, {'result': 42})
|
||||
self.assertTrue(task.name)
|
||||
self.assertTrue(self.cam.handle_task((task.uuid, task)))
|
||||
self.assertEqual(self.cam.on_cleanup(), expired)
|
||||
|
||||
def test_on_cleanup_expires(self, dec=332000):
|
||||
self.assertExpires(dec, 10)
|
||||
|
||||
def test_on_cleanup_does_not_expire_new(self, dec=0):
|
||||
self.assertExpires(dec, 0)
|
||||
|
||||
def test_on_shutter(self):
|
||||
state = self.state
|
||||
cam = self.cam
|
||||
|
||||
ws = ['worker1.ex.com', 'worker2.ex.com', 'worker3.ex.com']
|
||||
uus = [gen_unique_id() for i in range(50)]
|
||||
|
||||
events = [Event('worker-online', hostname=ws[0]),
|
||||
Event('worker-online', hostname=ws[1]),
|
||||
Event('worker-online', hostname=ws[2]),
|
||||
Event('task-received',
|
||||
uuid=uus[0], name='A', hostname=ws[0]),
|
||||
Event('task-started',
|
||||
uuid=uus[0], name='A', hostname=ws[0]),
|
||||
Event('task-received',
|
||||
uuid=uus[1], name='B', hostname=ws[1]),
|
||||
Event('task-revoked',
|
||||
uuid=uus[2], name='C', hostname=ws[2])]
|
||||
|
||||
for event in events:
|
||||
event['local_received'] = time()
|
||||
state.event(event)
|
||||
cam.on_shutter(state)
|
||||
|
||||
for host in ws:
|
||||
worker = models.WorkerState.objects.get(hostname=host)
|
||||
self.assertTrue(worker.is_alive())
|
||||
|
||||
t1 = models.TaskState.objects.get(task_id=uus[0])
|
||||
self.assertEqual(t1.state, 'STARTED')
|
||||
self.assertEqual(t1.name, 'A')
|
||||
t2 = models.TaskState.objects.get(task_id=uus[1])
|
||||
self.assertEqual(t2.state, 'RECEIVED')
|
||||
t3 = models.TaskState.objects.get(task_id=uus[2])
|
||||
self.assertEqual(t3.state, 'REVOKED')
|
||||
|
||||
events = [Event('task-succeeded',
|
||||
uuid=uus[0], hostname=ws[0], result=42),
|
||||
Event('task-failed',
|
||||
uuid=uus[1], exception="KeyError('foo')",
|
||||
hostname=ws[1]),
|
||||
Event('worker-offline', hostname=ws[0])]
|
||||
list(map(state.event, events))
|
||||
cam._last_worker_write.clear()
|
||||
cam.on_shutter(state)
|
||||
|
||||
w1 = models.WorkerState.objects.get(hostname=ws[0])
|
||||
self.assertFalse(w1.is_alive())
|
||||
|
||||
t1 = models.TaskState.objects.get(task_id=uus[0])
|
||||
self.assertEqual(t1.state, 'SUCCESS')
|
||||
self.assertEqual(t1.result, '42')
|
||||
self.assertEqual(t1.worker, w1)
|
||||
|
||||
t2 = models.TaskState.objects.get(task_id=uus[1])
|
||||
self.assertEqual(t2.state, 'FAILURE')
|
||||
self.assertEqual(t2.result, "KeyError('foo')")
|
||||
self.assertEqual(t2.worker.hostname, ws[1])
|
||||
|
||||
cam.on_shutter(state)
|
||||
@@ -0,0 +1,211 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from functools import partial
|
||||
|
||||
from billiard.einfo import ExceptionInfo
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.test.testcases import TestCase as DjangoTestCase
|
||||
from django.template import TemplateDoesNotExist
|
||||
|
||||
from anyjson import deserialize
|
||||
|
||||
from celery import current_app
|
||||
from celery import states
|
||||
from celery.task import task
|
||||
from celery.utils import gen_unique_id, get_full_cls_name
|
||||
|
||||
from djcelery.views import task_webhook
|
||||
from djcelery.tests.req import MockRequest
|
||||
|
||||
try:
|
||||
from django.urls import reverse # Django 1.10+
|
||||
except ImportError:
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
def reversestar(name, **kwargs):
|
||||
return reverse(name, kwargs=kwargs)
|
||||
|
||||
|
||||
class MyError(Exception):
|
||||
# On Py2.4 repr(exc) includes the object id, so comparing
|
||||
# texts is pointless when the id the "same" KeyError does not match.
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__}: {0.args!r}>'.format(self)
|
||||
|
||||
|
||||
class MyRetryTaskError(MyError):
|
||||
pass
|
||||
|
||||
|
||||
task_is_successful = partial(reversestar, 'celery-is_task_successful')
|
||||
task_status = partial(reversestar, 'celery-task_status')
|
||||
task_apply = partial(reverse, 'celery-apply')
|
||||
registered_tasks = partial(reverse, 'celery-tasks')
|
||||
scratch = {}
|
||||
|
||||
|
||||
@task()
|
||||
def mytask(x, y):
|
||||
ret = scratch['result'] = int(x) * int(y)
|
||||
return ret
|
||||
|
||||
|
||||
def create_exception(name, base=Exception):
|
||||
return type(name, (base, ), {})
|
||||
|
||||
|
||||
def catch_exception(exception):
|
||||
try:
|
||||
raise exception
|
||||
except exception.__class__ as exc:
|
||||
exc = current_app.backend.prepare_exception(exc)
|
||||
return exc, ExceptionInfo(sys.exc_info()).traceback
|
||||
|
||||
|
||||
class ViewTestCase(DjangoTestCase):
|
||||
|
||||
def assertJSONEqual(self, json, py):
|
||||
json = isinstance(json, HttpResponse) and json.content or json
|
||||
try:
|
||||
self.assertEqual(deserialize(json.decode('utf-8')), py)
|
||||
except TypeError as exc:
|
||||
raise TypeError('{0}: {1}'.format(exc, json))
|
||||
|
||||
def assertIn(self, expected, source, *args):
|
||||
try:
|
||||
DjangoTestCase.assertIn(self, expected, source, *args)
|
||||
except AttributeError:
|
||||
self.assertTrue(expected in source)
|
||||
|
||||
def assertDictContainsSubset(self, subset, dictionary, *args):
|
||||
for key, value in subset.items():
|
||||
self.assertIn(key, dictionary)
|
||||
self.assertEqual(dictionary[key], value)
|
||||
|
||||
|
||||
class test_task_apply(ViewTestCase):
|
||||
|
||||
def test_apply(self):
|
||||
current_app.conf.CELERY_ALWAYS_EAGER = True
|
||||
try:
|
||||
self.client.get(
|
||||
task_apply(kwargs={'task_name': mytask.name}) + '?x=4&y=4',
|
||||
)
|
||||
self.assertEqual(scratch['result'], 16)
|
||||
finally:
|
||||
current_app.conf.CELERY_ALWAYS_EAGER = False
|
||||
|
||||
def test_apply_raises_404_on_unregistered_task(self):
|
||||
current_app.conf.CELERY_ALWAYS_EAGER = True
|
||||
try:
|
||||
name = 'xxx.does.not.exist'
|
||||
action = partial(
|
||||
self.client.get,
|
||||
task_apply(kwargs={'task_name': name}) + '?x=4&y=4',
|
||||
)
|
||||
try:
|
||||
res = action()
|
||||
except TemplateDoesNotExist:
|
||||
pass # pre Django 1.5
|
||||
else:
|
||||
self.assertEqual(res.status_code, 404)
|
||||
finally:
|
||||
current_app.conf.CELERY_ALWAYS_EAGER = False
|
||||
|
||||
|
||||
class test_registered_tasks(ViewTestCase):
|
||||
|
||||
def test_list_registered_tasks(self):
|
||||
json = self.client.get(registered_tasks())
|
||||
tasks = deserialize(json.content.decode('utf-8'))
|
||||
self.assertIn('celery.backend_cleanup', tasks['regular'])
|
||||
|
||||
|
||||
class test_webhook_task(ViewTestCase):
|
||||
|
||||
def test_successful_request(self):
|
||||
|
||||
@task_webhook
|
||||
def add_webhook(request):
|
||||
x = int(request.GET['x'])
|
||||
y = int(request.GET['y'])
|
||||
return x + y
|
||||
|
||||
request = MockRequest().get('/tasks/add', dict(x=10, y=10))
|
||||
response = add_webhook(request)
|
||||
self.assertDictContainsSubset(
|
||||
{'status': 'success', 'retval': 20},
|
||||
deserialize(response.content.decode('utf-8')))
|
||||
|
||||
def test_failed_request(self):
|
||||
|
||||
@task_webhook
|
||||
def error_webhook(request):
|
||||
x = int(request.GET['x'])
|
||||
y = int(request.GET['y'])
|
||||
raise MyError(x + y)
|
||||
|
||||
request = MockRequest().get('/tasks/error', dict(x=10, y=10))
|
||||
response = error_webhook(request)
|
||||
self.assertDictContainsSubset(
|
||||
{'status': 'failure',
|
||||
'reason': '<MyError: (20,)>'},
|
||||
deserialize(response.content.decode('utf-8')))
|
||||
|
||||
|
||||
class test_task_status(ViewTestCase):
|
||||
|
||||
def assertStatusForIs(self, status, res, traceback=None):
|
||||
uuid = gen_unique_id()
|
||||
current_app.backend.store_result(uuid, res, status,
|
||||
traceback=traceback)
|
||||
json = self.client.get(task_status(task_id=uuid))
|
||||
expect = dict(id=uuid, status=status, result=res)
|
||||
if status in current_app.backend.EXCEPTION_STATES:
|
||||
instore = current_app.backend.get_result(uuid)
|
||||
self.assertEqual(str(instore.args[0]), str(res.args[0]))
|
||||
expect['result'] = repr(res)
|
||||
expect['exc'] = get_full_cls_name(res.__class__)
|
||||
expect['traceback'] = traceback
|
||||
|
||||
self.assertJSONEqual(json, dict(task=expect))
|
||||
|
||||
def test_success(self):
|
||||
self.assertStatusForIs(states.SUCCESS, 'The quick brown fox')
|
||||
|
||||
def test_failure(self):
|
||||
exc, tb = catch_exception(MyError('foo'))
|
||||
self.assertStatusForIs(states.FAILURE, exc, tb)
|
||||
|
||||
def test_retry(self):
|
||||
oexc, _ = catch_exception(MyError('Resource not available'))
|
||||
exc, tb = catch_exception(MyRetryTaskError(str(oexc), oexc))
|
||||
self.assertStatusForIs(states.RETRY, exc, tb)
|
||||
|
||||
|
||||
class test_task_is_successful(ViewTestCase):
|
||||
|
||||
def assertStatusForIs(self, status, outcome, result=None):
|
||||
uuid = gen_unique_id()
|
||||
result = result or gen_unique_id()
|
||||
current_app.backend.store_result(uuid, result, status)
|
||||
json = self.client.get(task_is_successful(task_id=uuid))
|
||||
self.assertJSONEqual(json, {'task': {'id': uuid,
|
||||
'executed': outcome}})
|
||||
|
||||
def test_success(self):
|
||||
self.assertStatusForIs(states.SUCCESS, True)
|
||||
|
||||
def test_pending(self):
|
||||
self.assertStatusForIs(states.PENDING, False)
|
||||
|
||||
def test_failure(self):
|
||||
self.assertStatusForIs(states.FAILURE, False, KeyError('foo'))
|
||||
|
||||
def test_retry(self):
|
||||
self.assertStatusForIs(states.RETRY, False, KeyError('foo'))
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core import cache
|
||||
|
||||
from celery.utils import gen_unique_id
|
||||
from celery.task import task as task_dec
|
||||
|
||||
from celery.tests.worker.test_request import jail
|
||||
|
||||
from djcelery.app import app
|
||||
from djcelery.tests.utils import unittest
|
||||
|
||||
|
||||
@task_dec()
|
||||
def mytask(i):
|
||||
return i ** i
|
||||
|
||||
|
||||
@task_dec()
|
||||
def get_db_connection(i):
|
||||
from django.db import connection
|
||||
return id(connection)
|
||||
|
||||
|
||||
get_db_connection.ignore_result = True
|
||||
|
||||
|
||||
class TestJail(unittest.TestCase):
|
||||
|
||||
def test_django_db_connection_is_closed(self):
|
||||
from django.db import connection
|
||||
connection._was_closed = False
|
||||
old_connection_close = connection.close
|
||||
|
||||
def monkeypatched_connection_close(*args, **kwargs):
|
||||
connection._was_closed = True
|
||||
return old_connection_close(*args, **kwargs)
|
||||
|
||||
connection.close = monkeypatched_connection_close
|
||||
try:
|
||||
jail(app, gen_unique_id(), get_db_connection.name, [2], {})
|
||||
self.assertTrue(connection._was_closed)
|
||||
finally:
|
||||
connection.close = old_connection_close
|
||||
|
||||
def test_django_cache_connection_is_closed(self):
|
||||
old_cache_close = getattr(cache.cache, 'close', None)
|
||||
cache._was_closed = False
|
||||
old_cache_parse_backend = getattr(cache, 'parse_backend_uri', None)
|
||||
if old_cache_parse_backend: # checks to make sure attr exists
|
||||
delattr(cache, 'parse_backend_uri')
|
||||
|
||||
def monkeypatched_cache_close(*args, **kwargs):
|
||||
cache._was_closed = True
|
||||
|
||||
cache.cache.close = monkeypatched_cache_close
|
||||
|
||||
jail(app, gen_unique_id(), mytask.name, [4], {})
|
||||
self.assertTrue(cache._was_closed)
|
||||
cache.cache.close = old_cache_close
|
||||
if old_cache_parse_backend:
|
||||
cache.parse_backend_uri = old_cache_parse_backend
|
||||
|
||||
def test_django_cache_connection_is_closed_django_1_1(self):
|
||||
old_cache_close = getattr(cache.cache, 'close', None)
|
||||
cache._was_closed = False
|
||||
old_cache_parse_backend = getattr(cache, 'parse_backend_uri', None)
|
||||
cache.parse_backend_uri = lambda uri: ['libmemcached', '1', '2']
|
||||
|
||||
def monkeypatched_cache_close(*args, **kwargs):
|
||||
cache._was_closed = True
|
||||
|
||||
cache.cache.close = monkeypatched_cache_close
|
||||
|
||||
jail(app, gen_unique_id(), mytask.name, [4], {})
|
||||
self.assertTrue(cache._was_closed)
|
||||
cache.cache.close = old_cache_close
|
||||
if old_cache_parse_backend:
|
||||
cache.parse_backend_uri = old_cache_parse_backend
|
||||
else:
|
||||
del(cache.parse_backend_uri)
|
||||
@@ -0,0 +1,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
try:
|
||||
import unittest
|
||||
unittest.skip
|
||||
except AttributeError:
|
||||
import unittest2 as unittest # noqa
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
|
||||
This module is an alias to :mod:`kombu.transport.django`
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import kombu.transport.django as transport
|
||||
|
||||
__path__.extend(transport.__path__)
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
|
||||
URLs defined for celery.
|
||||
|
||||
* ``/$task_id/done/``
|
||||
|
||||
URL to :func:`~celery.views.is_successful`.
|
||||
|
||||
* ``/$task_id/status/``
|
||||
|
||||
URL to :func:`~celery.views.task_status`.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
|
||||
from . import views
|
||||
|
||||
task_pattern = r'(?P<task_id>[\w\d\-\.]+)'
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^%s/done/?$' % task_pattern,
|
||||
views.is_task_successful,
|
||||
name='celery-is_task_successful'
|
||||
),
|
||||
url(
|
||||
r'^%s/status/?$' % task_pattern,
|
||||
views.task_status,
|
||||
name='celery-task_status'
|
||||
),
|
||||
url(
|
||||
r'^tasks/?$',
|
||||
views.registered_tasks,
|
||||
name='celery-tasks'
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
# -- XXX This module must not use translation as that causes
|
||||
# -- a recursive loader import!
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
# Database-related exceptions.
|
||||
from django.db import 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
|
||||
|
||||
DATABASE_ERRORS = ((DatabaseError, ) +
|
||||
_my_database_errors +
|
||||
_pg_database_errors +
|
||||
_lite_database_errors +
|
||||
_oracle_database_errors)
|
||||
|
||||
|
||||
def make_aware(value):
|
||||
if settings.USE_TZ:
|
||||
# naive datetimes are assumed to be in UTC.
|
||||
if timezone.is_naive(value):
|
||||
value = timezone.make_aware(value, timezone.utc)
|
||||
# then convert to the Django configured timezone.
|
||||
default_tz = timezone.get_default_timezone()
|
||||
value = timezone.localtime(value, default_tz)
|
||||
return value
|
||||
|
||||
|
||||
def make_naive(value):
|
||||
if settings.USE_TZ:
|
||||
default_tz = timezone.get_default_timezone()
|
||||
value = timezone.make_naive(value, default_tz)
|
||||
return value
|
||||
|
||||
|
||||
def now():
|
||||
return make_aware(timezone.now())
|
||||
|
||||
|
||||
def correct_awareness(value):
|
||||
if isinstance(value, datetime):
|
||||
if settings.USE_TZ:
|
||||
return make_aware(value)
|
||||
elif timezone.is_aware(value):
|
||||
default_tz = timezone.get_default_timezone()
|
||||
return timezone.make_naive(value, default_tz)
|
||||
return value
|
||||
|
||||
|
||||
def is_database_scheduler(scheduler):
|
||||
if not scheduler:
|
||||
return False
|
||||
from kombu.utils import symbol_by_name
|
||||
from .schedulers import DatabaseScheduler
|
||||
return issubclass(symbol_by_name(scheduler), DatabaseScheduler)
|
||||
|
||||
|
||||
def fromtimestamp(value):
|
||||
if settings.USE_TZ:
|
||||
return make_aware(datetime.utcfromtimestamp(value))
|
||||
else:
|
||||
return datetime.fromtimestamp(value)
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from django.http import HttpResponse, Http404
|
||||
|
||||
from anyjson import serialize
|
||||
|
||||
from celery import states
|
||||
from celery.five import keys, items
|
||||
from celery.registry import tasks
|
||||
from celery.result import AsyncResult
|
||||
from celery.utils import get_full_cls_name
|
||||
from celery.utils.encoding import safe_repr
|
||||
|
||||
# Ensure built-in tasks are loaded for task_list view
|
||||
import celery.task # noqa
|
||||
|
||||
|
||||
def JsonResponse(response):
|
||||
return HttpResponse(serialize(response), content_type='application/json')
|
||||
|
||||
|
||||
def task_view(task):
|
||||
"""Decorator turning any task into a view that applies the task
|
||||
asynchronously. Keyword arguments (via URLconf, etc.) will
|
||||
supercede GET or POST parameters when there are conflicts.
|
||||
|
||||
Returns a JSON dictionary containing the keys ``ok``, and
|
||||
``task_id``.
|
||||
|
||||
"""
|
||||
|
||||
def _applier(request, **options):
|
||||
kwargs = request.POST if request.method == 'POST' else request.GET
|
||||
# no multivalue
|
||||
kwargs = {k: v for k, v in items(kwargs)}
|
||||
if options:
|
||||
kwargs.update(options)
|
||||
result = task.apply_async(kwargs=kwargs)
|
||||
return JsonResponse({'ok': 'true', 'task_id': result.task_id})
|
||||
|
||||
return _applier
|
||||
|
||||
|
||||
def apply(request, task_name):
|
||||
"""View applying a task.
|
||||
|
||||
**Note:** Please use this with caution. Preferably you shouldn't make this
|
||||
publicly accessible without ensuring your code is safe!
|
||||
|
||||
"""
|
||||
try:
|
||||
task = tasks[task_name]
|
||||
except KeyError:
|
||||
raise Http404('apply: no such task')
|
||||
return task_view(task)(request)
|
||||
|
||||
|
||||
def is_task_successful(request, task_id):
|
||||
"""Returns task execute status in JSON format."""
|
||||
return JsonResponse({'task': {
|
||||
'id': task_id,
|
||||
'executed': AsyncResult(task_id).successful(),
|
||||
}})
|
||||
|
||||
|
||||
def task_status(request, task_id):
|
||||
"""Returns task status and result in JSON format."""
|
||||
result = AsyncResult(task_id)
|
||||
state, retval = result.state, result.result
|
||||
response_data = {'id': task_id, 'status': state, 'result': retval}
|
||||
if state in states.EXCEPTION_STATES:
|
||||
traceback = result.traceback
|
||||
response_data.update({'result': safe_repr(retval),
|
||||
'exc': get_full_cls_name(retval.__class__),
|
||||
'traceback': traceback})
|
||||
return JsonResponse({'task': response_data})
|
||||
|
||||
|
||||
def registered_tasks(request):
|
||||
"""View returning all defined tasks as a JSON object."""
|
||||
return JsonResponse({'regular': list(keys(tasks)), 'periodic': ''})
|
||||
|
||||
|
||||
def task_webhook(fun):
|
||||
"""Decorator turning a function into a task webhook.
|
||||
|
||||
If an exception is raised within the function, the decorated
|
||||
function catches this and returns an error JSON response, otherwise
|
||||
it returns the result as a JSON response.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@task_webhook
|
||||
def add(request):
|
||||
x = int(request.GET['x'])
|
||||
y = int(request.GET['y'])
|
||||
return x + y
|
||||
|
||||
def view(request):
|
||||
response = add(request)
|
||||
print(response.content)
|
||||
|
||||
Gives::
|
||||
|
||||
"{'status': 'success', 'retval': 100}"
|
||||
|
||||
"""
|
||||
|
||||
@wraps(fun)
|
||||
def _inner(*args, **kwargs):
|
||||
try:
|
||||
retval = fun(*args, **kwargs)
|
||||
except Exception as exc:
|
||||
response = {'status': 'failure', 'reason': safe_repr(exc)}
|
||||
else:
|
||||
response = {'status': 'success', 'retval': retval}
|
||||
|
||||
return JsonResponse(response)
|
||||
|
||||
return _inner
|
||||
Reference in New Issue
Block a user