added scheduled tasks with admin interface

This commit is contained in:
Ilan Steemers
2015-06-25 19:48:38 +02:00
parent 6314742343
commit d06de5769b
7 changed files with 156 additions and 17 deletions
+14 -1
View File
@@ -2,7 +2,7 @@ from django.contrib import admin
from django_q.core import async from django_q.core import async
from .models import Success, Failure from .models import Success, Failure, Schedule
class TaskAdmin(admin.ModelAdmin): class TaskAdmin(admin.ModelAdmin):
@@ -59,6 +59,19 @@ class FailAdmin(admin.ModelAdmin):
return list(self.readonly_fields) + \ return list(self.readonly_fields) + \
[field.name for field in obj._meta.fields] [field.name for field in obj._meta.fields]
class ScheduleAdmin(admin.ModelAdmin):
list_display = (
u'id',
'func',
'schedule_type',
'repeats',
'next_run',
'result',
'success'
)
list_filter = ('next_run', 'schedule_type')
admin.site.register(Schedule, ScheduleAdmin)
admin.site.register(Success, TaskAdmin) admin.site.register(Success, TaskAdmin)
admin.site.register(Failure, FailAdmin) admin.site.register(Failure, FailAdmin)
+50 -1
View File
@@ -3,8 +3,10 @@ from __future__ import unicode_literals
from __future__ import print_function from __future__ import print_function
from __future__ import division from __future__ import division
from __future__ import absolute_import from __future__ import absolute_import
import ast
from builtins import dict from builtins import dict
from builtins import range from builtins import range
from future import standard_library from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
@@ -28,6 +30,7 @@ except ImportError:
# External # External
import coloredlogs import coloredlogs
import redis import redis
import arrow
# Django # Django
from django.core import signing from django.core import signing
@@ -36,7 +39,7 @@ from django.utils import timezone
# Local # Local
from .conf import LOG_LEVEL, SECRET_KEY, SAVE_LIMIT, WORKERS, COMPRESSED, PREFIX from .conf import LOG_LEVEL, SECRET_KEY, SAVE_LIMIT, WORKERS, COMPRESSED, PREFIX
from .humanhash import uuid from .humanhash import uuid
from .models import Task, Success from .models import Task, Success, Schedule
SIGNAL_NAMES = dict((getattr(signal, n), n) for n in dir(signal) if n.startswith('SIG') and '_' not in n) SIGNAL_NAMES = dict((getattr(signal, n), n) for n in dir(signal) if n.startswith('SIG') and '_' not in n)
@@ -197,6 +200,7 @@ class Sentinel(object):
self.start_event.set() self.start_event.set()
self.set_status(RUNNING) self.set_status(RUNNING)
logger.info('Q Cluster-{} running.'.format(self.parent_pid)) logger.info('Q Cluster-{} running.'.format(self.parent_pid))
counter = 0
while True: while True:
for p in list(self.pool): for p in list(self.pool):
if not p.is_alive(): if not p.is_alive():
@@ -206,6 +210,10 @@ class Sentinel(object):
Stat(self).save() Stat(self).save()
if self.stop_event.is_set(): if self.stop_event.is_set():
break break
counter += 1
if counter > 15:
counter = 0
scheduler()
sleep(2) sleep(2)
self.stop() self.stop()
@@ -438,3 +446,44 @@ class Stat(Status):
except signing.BadSignature: except signing.BadSignature:
continue continue
return stats return stats
def scheduler():
for schedule in Schedule.objects.exclude(repeats=0).filter(next_run__lt=timezone.now()):
args = ()
kwargs= {}
if schedule.kwargs:
try:
kwargs = eval('dict({})'.format(schedule.kwargs))
except SyntaxError:
kwargs={}
if schedule.args:
args = ast.literal_eval(schedule.args)
if type(args) != tuple:
args = (args,)
if schedule.hook:
kwargs['hook'] = schedule.hook
schedule.task = async(schedule.func, *args, **kwargs)
if not schedule.schedule_type == schedule.ONCE:
next_run = arrow.get(schedule.next_run)
if schedule.schedule_type == schedule.HOURLY:
next_run = next_run.replace(hours=+1)
elif schedule.schedule_type == schedule.DAILY:
next_run = next_run.replace(days=+1)
elif schedule.schedule_type == schedule.WEEKLY:
next_run = next_run.replace(weeks=+1)
elif schedule.schedule_type == schedule.MONTHLY:
next_run = next_run.replace(months=+1)
elif schedule.schedule_type == schedule.QUARTERLY:
next_run = next_run.replace(months=+3)
elif schedule.schedule_type == schedule.YEARLY:
next_run = next_run.replace(years=+1)
schedule.next_run = next_run.datetime
schedule.repeats += -1
else:
schedule.repeats = 0
if not schedule.task:
logger.error('{} failed to create task from schedule {}').format(current_process().name, schedule.id)
else:
logger.info('{} created [{}] from schedule {}'.format(current_process().name, schedule.task, schedule.id))
schedule.save()
+26 -7
View File
@@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
import django.utils.timezone
import picklefield.fields import picklefield.fields
@@ -11,19 +12,37 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='Schedule',
fields=[
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
('func', models.CharField(max_length=256)),
('hook', models.CharField(blank=True, max_length=256, null=True)),
('args', models.CharField(blank=True, max_length=256, null=True)),
('kwargs', models.CharField(blank=True, max_length=256, null=True)),
('schedule_type', models.CharField(choices=[('O', 'Once'), ('H', 'Hourly'), ('D', 'Daily'), ('W', 'Weekly'), ('M', 'Monthly'), ('Q', 'Quarterly'), ('Y', 'Yearly')], max_length=1, verbose_name='Schedule Type', default='O')),
('repeats', models.SmallIntegerField(verbose_name='Repeats', default=-1)),
('next_run', models.DateTimeField(null=True, verbose_name='Next Run', default=django.utils.timezone.now)),
('task', models.CharField(editable=False, max_length=100, null=True)),
],
options={
'ordering': ['next_run'],
'verbose_name': 'Scheduled task',
},
),
migrations.CreateModel( migrations.CreateModel(
name='Task', name='Task',
fields=[ fields=[
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
('name', models.CharField(max_length=100)), ('name', models.CharField(editable=False, max_length=100)),
('func', models.CharField(max_length=256)), ('func', models.CharField(max_length=256)),
('hook', models.CharField(max_length=256, null=True)), ('hook', models.CharField(max_length=256, null=True)),
('args', picklefield.fields.PickledObjectField(editable=False)), ('args', picklefield.fields.PickledObjectField(editable=False)),
('kwargs', picklefield.fields.PickledObjectField(editable=False)), ('kwargs', picklefield.fields.PickledObjectField(editable=False)),
('result', picklefield.fields.PickledObjectField(null=True, editable=False)), ('result', picklefield.fields.PickledObjectField(editable=False, null=True)),
('started', models.DateTimeField()), ('started', models.DateTimeField(editable=False)),
('stopped', models.DateTimeField()), ('stopped', models.DateTimeField(editable=False)),
('success', models.BooleanField(default=True)), ('success', models.BooleanField(editable=False, default=True)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@@ -31,8 +50,8 @@ class Migration(migrations.Migration):
fields=[ fields=[
], ],
options={ options={
'proxy': True,
'verbose_name': 'Failed task', 'verbose_name': 'Failed task',
'proxy': True,
}, },
bases=('django_q.task',), bases=('django_q.task',),
), ),
@@ -41,8 +60,8 @@ class Migration(migrations.Migration):
fields=[ fields=[
], ],
options={ options={
'proxy': True,
'verbose_name': 'Successful task', 'verbose_name': 'Successful task',
'proxy': True,
}, },
bases=('django_q.task',), bases=('django_q.task',),
), ),
+61 -7
View File
@@ -1,22 +1,25 @@
import importlib import importlib
import logging import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.db import models from django.db import models
from django.db.models.signals import pre_save from django.db.models.signals import pre_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from picklefield import PickledObjectField from picklefield import PickledObjectField
class Task(models.Model): class Task(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100, editable=False)
func = models.CharField(max_length=256) func = models.CharField(max_length=256)
hook = models.CharField(max_length=256, null=True) hook = models.CharField(max_length=256, null=True)
args = PickledObjectField() args = PickledObjectField()
kwargs = PickledObjectField() kwargs = PickledObjectField()
result = PickledObjectField(null=True) result = PickledObjectField(null=True)
started = models.DateTimeField() started = models.DateTimeField(editable=False)
stopped = models.DateTimeField() stopped = models.DateTimeField(editable=False)
success = models.BooleanField(default=True) success = models.BooleanField(default=True, editable=False)
@staticmethod @staticmethod
def get_result(name): def get_result(name):
@@ -30,6 +33,7 @@ class Task(models.Model):
class Meta: class Meta:
app_label = 'django_q' app_label = 'django_q'
@receiver(pre_save, sender=Task) @receiver(pre_save, sender=Task)
def call_hook(sender, instance, **kwargs): def call_hook(sender, instance, **kwargs):
if instance.hook: if instance.hook:
@@ -40,7 +44,7 @@ def call_hook(sender, instance, **kwargs):
f(instance) f(instance)
except Exception as e: except Exception as e:
logger = logging.getLogger('django-q') logger = logging.getLogger('django-q')
logger.error('return hook failed on {}'.format(instance.name)) logger.error(_('return hook failed on {}').format(instance.name))
logger.exception(e) logger.exception(e)
@@ -55,7 +59,7 @@ class Success(Task):
class Meta: class Meta:
app_label = 'django_q' app_label = 'django_q'
verbose_name = 'Successful task' verbose_name = _('Successful task')
proxy = True proxy = True
@@ -70,5 +74,55 @@ class Failure(Task):
class Meta: class Meta:
app_label = 'django_q' app_label = 'django_q'
verbose_name = 'Failed task' verbose_name = _('Failed task')
proxy = True proxy = True
class Schedule(models.Model):
func = models.CharField(max_length=256)
hook = models.CharField(max_length=256, null=True, blank=True)
args = models.CharField(max_length=256, null=True, blank=True)
kwargs = models.CharField(max_length=256, null=True, blank=True)
ONCE = 'O'
HOURLY = 'H'
DAILY = 'D'
WEEKLY = 'W'
MONTHLY = 'M'
QUARTERLY = 'Q'
YEARLY = 'Y'
TYPE = (
(ONCE, _('Once')),
(HOURLY, _('Hourly')),
(DAILY, _('Daily')),
(WEEKLY, _('Weekly')),
(MONTHLY, _('Monthly')),
(QUARTERLY, _('Quarterly')),
(YEARLY, _('Yearly')),
)
schedule_type = models.CharField(max_length=1, choices=TYPE, default=TYPE[0][0], verbose_name=_('Schedule Type'))
repeats = models.SmallIntegerField(default=-1, verbose_name=_('Repeats'))
next_run = models.DateTimeField(verbose_name=_('Next Run'), default=timezone.now, null=True)
task = models.CharField(max_length=100, editable=False, null=True)
def result(self):
if Task.objects.filter(name=self.task).exists():
task = Task.objects.get(name=self.task)
if task.success:
url = reverse('admin:django_q_success_change', args=(task.id,))
else:
url = reverse('admin:django_q_failure_change', args=(task.id,))
return '<a href="{}">[{}]</a>'.format(url, self.task)
return None
def success(self):
if Task.objects.filter(name=self.task).exists():
return Task.objects.get(name=self.task).success
success.boolean = True
result.allow_tags = True
class Meta:
app_label = 'django_q'
verbose_name = _('Scheduled task')
ordering = ['next_run']
+3
View File
@@ -3,6 +3,9 @@ def countdown(n):
while n > 0: while n > 0:
n -= 1 n -= 1
def multiply(x, y):
return x * y
def count_letters(tup): def count_letters(tup):
total = 0 total = 0
for word in tup: for word in tup:
+1
View File
@@ -1,3 +1,4 @@
arrow
blessed==1.9.5 blessed==1.9.5
coloredlogs==1.0.1 coloredlogs==1.0.1
django-picklefield==0.3.1 django-picklefield==0.3.1
+1 -1
View File
@@ -35,7 +35,7 @@ setup(
license='MIT', license='MIT',
description='A multiprocessing task queue for Django', description='A multiprocessing task queue for Django',
long_description=README, long_description=README,
install_requires=['django>=1.7', 'redis', 'coloredlogs', 'django-picklefield', 'blessed'], install_requires=['django>=1.7', 'redis', 'coloredlogs', 'django-picklefield', 'blessed', 'arrow'],
test_requires=['pytest', 'pytest-django', ], test_requires=['pytest', 'pytest-django', ],
cmdclass={'test': PyTest}, cmdclass={'test': PyTest},
classifiers=[ classifiers=[