Merge pull request #21 from Koed00/dev

Adds task groups
This commit is contained in:
Ilan Steemers
2015-07-19 02:42:41 -07:00
11 changed files with 176 additions and 29 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import sys
myPath = os.path.dirname(os.path.abspath(__file__)) myPath = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, myPath) sys.path.insert(0, myPath)
from .tasks import async, schedule, result, fetch from .tasks import async, schedule, result, result_group, fetch, fetch_group
from .models import Task, Schedule, Success, Failure from .models import Task, Schedule, Success, Failure
from .cluster import Cluster from .cluster import Cluster
+5 -3
View File
@@ -11,7 +11,8 @@ class TaskAdmin(admin.ModelAdmin):
'func', 'func',
'started', 'started',
'stopped', 'stopped',
'time_taken' 'time_taken',
'group'
) )
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):
@@ -23,7 +24,7 @@ class TaskAdmin(admin.ModelAdmin):
qs = super(TaskAdmin, self).get_queryset(request) qs = super(TaskAdmin, self).get_queryset(request)
return qs.filter(success=True) return qs.filter(success=True)
search_fields = ('name', 'func') search_fields = ('name', 'func', 'group')
readonly_fields = [] readonly_fields = []
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
@@ -65,6 +66,7 @@ class FailAdmin(admin.ModelAdmin):
class ScheduleAdmin(admin.ModelAdmin): class ScheduleAdmin(admin.ModelAdmin):
list_display = ( list_display = (
'id', 'id',
'name',
'func', 'func',
'schedule_type', 'schedule_type',
'repeats', 'repeats',
@@ -75,7 +77,7 @@ class ScheduleAdmin(admin.ModelAdmin):
list_filter = ('next_run', 'schedule_type') list_filter = ('next_run', 'schedule_type')
search_fields = ('func',) search_fields = ('func',)
list_display_links = ('id', 'func') list_display_links = ('id', 'name')
admin.site.register(Schedule, ScheduleAdmin) admin.site.register(Schedule, ScheduleAdmin)
+7 -4
View File
@@ -404,12 +404,13 @@ def save_task(task):
Task.objects.create(id=task['id'], Task.objects.create(id=task['id'],
name=task['name'], name=task['name'],
func=task['func'], func=task['func'],
hook=task['hook'], hook=task.get('hook'),
args=task['args'], args=task['args'],
kwargs=task['kwargs'], kwargs=task['kwargs'],
started=task['started'], started=task['started'],
stopped=task['stopped'], stopped=task['stopped'],
result=task['result'], result=task['result'],
group=task.get('group'),
success=task['success']) success=task['success'])
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@@ -455,13 +456,15 @@ def scheduler(list_key=Conf.Q_LIST):
s.repeats += -1 s.repeats += -1
# send it to the cluster # send it to the cluster
kwargs['list_key'] = list_key kwargs['list_key'] = list_key
kwargs['group'] = s.name or s.id
s.task = tasks.async(s.func, *args, **kwargs) s.task = tasks.async(s.func, *args, **kwargs)
# log it # log it
if not s.task: if not s.task:
logger.error(_('{} failed to create a task from schedule {} [{}]').format(current_process().name, s.id), logger.error(
s.func) _('{} failed to create a task from schedule [{}]').format(current_process().name, s.name or s.id))
else: else:
logger.info(_('{} created a task from schedule {} [{}]').format(current_process().name, s.id, s.func)) logger.info(
_('{} created a task from schedule [{}]').format(current_process().name, s.name or s.id))
# default behavior is to delete a ONCE schedule # default behavior is to delete a ONCE schedule
if s.schedule_type == s.ONCE: if s.schedule_type == s.ONCE:
if s.repeats < 0: if s.repeats < 0:
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('django_q', '0004_auto_20150710_1043'),
]
operations = [
migrations.AddField(
model_name='schedule',
name='name',
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name='task',
name='group',
field=models.CharField(max_length=100, null=True, editable=False),
),
]
+12 -1
View File
@@ -1,6 +1,6 @@
import importlib
import logging import logging
import importlib
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import models from django.db import models
@@ -18,6 +18,7 @@ class Task(models.Model):
args = PickledObjectField(null=True) args = PickledObjectField(null=True)
kwargs = PickledObjectField(null=True) kwargs = PickledObjectField(null=True)
result = PickledObjectField(null=True) result = PickledObjectField(null=True)
group = models.CharField(max_length=100, editable=False, null=True)
started = models.DateTimeField(editable=False) started = models.DateTimeField(editable=False)
stopped = models.DateTimeField(editable=False) stopped = models.DateTimeField(editable=False)
success = models.BooleanField(default=True, editable=False) success = models.BooleanField(default=True, editable=False)
@@ -29,6 +30,11 @@ class Task(models.Model):
elif Task.objects.filter(name=task_id).exists(): elif Task.objects.filter(name=task_id).exists():
return Task.objects.get(name=task_id).result return Task.objects.get(name=task_id).result
@staticmethod
def get_result_group(group_id):
# values_list() doesn't work here cause it returns encoded fields
return [t.result for t in Task.get_task_group(group_id)]
@staticmethod @staticmethod
def get_task(task_id): def get_task(task_id):
if len(task_id) == 32 and Task.objects.filter(id=task_id).exists(): if len(task_id) == 32 and Task.objects.filter(id=task_id).exists():
@@ -36,6 +42,10 @@ class Task(models.Model):
elif Task.objects.filter(name=task_id).exists(): elif Task.objects.filter(name=task_id).exists():
return Task.objects.get(name=task_id) return Task.objects.get(name=task_id)
@staticmethod
def get_task_group(group_id):
return Task.objects.filter(group=group_id)
def time_taken(self): def time_taken(self):
return (self.stopped - self.started).total_seconds() return (self.stopped - self.started).total_seconds()
@@ -101,6 +111,7 @@ class Failure(Task):
class Schedule(models.Model): class Schedule(models.Model):
name = models.CharField(max_length=100, null=True)
func = models.CharField(max_length=256, help_text='e.g. module.tasks.function') func = models.CharField(max_length=256, help_text='e.g. module.tasks.function')
hook = models.CharField(max_length=256, null=True, blank=True, help_text='e.g. module.tasks.result_function') hook = models.CharField(max_length=256, null=True, blank=True, help_text='e.g. module.tasks.result_function')
args = models.TextField(null=True, blank=True, help_text=_("e.g. 1, 2, 'John'")) args = models.TextField(null=True, blank=True, help_text=_("e.g. 1, 2, 'John'"))
+35 -6
View File
@@ -25,20 +25,27 @@ def async(func, *args, **kwargs):
# optional list_key # optional list_key
list_key = kwargs.pop('list_key', Conf.Q_LIST) list_key = kwargs.pop('list_key', Conf.Q_LIST)
# optional redis connection # optional redis connection
r = kwargs.pop('redis', redis_client) redis = kwargs.pop('redis', redis_client)
# optional sync mode # optional sync mode
s = kwargs.pop('sync', False) sync = kwargs.pop('sync', False)
# optional group
group = kwargs.pop('group', None)
# get an id # get an id
tag = uuid() tag = uuid()
# build the task package # build the task package
task = {'id': tag[1], 'name': tag[0], 'func': func, 'hook': hook, 'args': args, 'kwargs': kwargs, task = {'id': tag[1], 'name': tag[0], 'func': func, 'args': args, 'kwargs': kwargs,
'started': timezone.now()} 'started': timezone.now()}
# add optionals
if hook:
task['hook'] = hook
if group:
task['group'] = group
# sign it # sign it
pack = signing.SignedPackage.dumps(task) pack = signing.SignedPackage.dumps(task)
if s: if sync:
return _sync(task['id'], pack) return _sync(task['id'], pack)
# push it # push it
r.rpush(list_key, pack) redis.rpush(list_key, pack)
logger.debug('Pushed {}'.format(tag)) logger.debug('Pushed {}'.format(tag))
return task['id'] return task['id']
@@ -47,6 +54,7 @@ def schedule(func, *args, **kwargs):
""" """
:param func: function to schedule :param func: function to schedule
:param args: function arguments :param args: function arguments
:param name: optional name for the schedule
:param hook: optional result hook function :param hook: optional result hook function
:type schedule_type: Schedule.TYPE :type schedule_type: Schedule.TYPE
:param repeats: how many times to repeat. 0=never, -1=always :param repeats: how many times to repeat. 0=never, -1=always
@@ -57,12 +65,14 @@ def schedule(func, *args, **kwargs):
:rtype: Schedule :rtype: Schedule
""" """
name = kwargs.pop('name', None)
hook = kwargs.pop('hook', None) hook = kwargs.pop('hook', None)
schedule_type = kwargs.pop('schedule_type', Schedule.ONCE) schedule_type = kwargs.pop('schedule_type', Schedule.ONCE)
repeats = kwargs.pop('repeats', -1) repeats = kwargs.pop('repeats', -1)
next_run = kwargs.pop('next_run', timezone.now()) next_run = kwargs.pop('next_run', timezone.now())
return Schedule.objects.create(func=func, return Schedule.objects.create(name=name,
func=func,
hook=hook, hook=hook,
args=args, args=args,
kwargs=kwargs, kwargs=kwargs,
@@ -83,6 +93,15 @@ def result(task_id):
return Task.get_result(task_id) return Task.get_result(task_id)
def result_group(group_id):
"""
returns a list of results for a task group
:param str group_id: the group id
:return: list or results
"""
return Task.get_result_group(group_id)
def fetch(task_id): def fetch(task_id):
""" """
Returns the processed task Returns the processed task
@@ -94,6 +113,16 @@ def fetch(task_id):
return Task.get_task(task_id) return Task.get_task(task_id)
def fetch_group(group_id):
"""
Returns a list of Tasks for a task group
:param str group_id: the group id
:return: list of Tasks
"""
return Task.get_task_group(group_id)
def _sync(task_id, pack): def _sync(task_id, pack):
""" """
Simulates a package travelling through the cluster. Simulates a package travelling through the cluster.
+5 -3
View File
@@ -10,7 +10,7 @@ sys.path.insert(0, myPath + '/../')
from django_q.cluster import Cluster, Sentinel, pusher, worker, monitor from django_q.cluster import Cluster, Sentinel, pusher, worker, monitor
from django_q.humanhash import DEFAULT_WORDLIST from django_q.humanhash import DEFAULT_WORDLIST
from django_q.tasks import fetch, async, result from django_q.tasks import fetch, fetch_group, async, result, result_group
from django_q.models import Task from django_q.models import Task
from django_q.conf import Conf, redis_client from django_q.conf import Conf, redis_client
from .tasks import multiply from .tasks import multiply
@@ -119,10 +119,10 @@ def test_async(r, admin_user):
f = async(multiply, 753, 2, hook=assert_result, list_key=list_key, redis=r) f = async(multiply, 753, 2, hook=assert_result, list_key=list_key, redis=r)
# model as argument # model as argument
g = async('django_q.tests.tasks.get_task_name', Task(name='John'), list_key=list_key, redis=r) g = async('django_q.tests.tasks.get_task_name', Task(name='John'), list_key=list_key, redis=r)
# args and kwargs and broken hook # args,kwargs, group and broken hook
h = async('django_q.tests.tasks.word_multiply', 2, word='django', hook='fail.me', list_key=list_key, redis=r) h = async('django_q.tests.tasks.word_multiply', 2, word='django', hook='fail.me', list_key=list_key, redis=r)
# args unpickle test # args unpickle test
j = async('django_q.tests.tasks.get_user_id', admin_user, list_key=list_key, redis=r) j = async('django_q.tests.tasks.get_user_id', admin_user, list_key=list_key, group='test_j', redis=r)
# check if everything has a task id # check if everything has a task id
assert isinstance(a, str) assert isinstance(a, str)
assert isinstance(b, str) assert isinstance(b, str)
@@ -197,6 +197,8 @@ def test_async(r, admin_user):
assert result_j is not None assert result_j is not None
assert result_j.success is True assert result_j.success is True
assert result_j.result == result_j.args[0].id assert result_j.result == result_j.args[0].id
assert result_group('test_j') == [result_j.result]
assert fetch_group('test_j')[0].id == [result_j][0].id
r.delete(list_key) r.delete(list_key)
+1
View File
@@ -20,6 +20,7 @@ def test_scheduler(r):
r.delete(list_key) r.delete(list_key)
schedule = create_schedule('math.copysign', schedule = create_schedule('math.copysign',
1, -1, 1, -1,
name='test math',
hook='django_q.tests.tasks.result', hook='django_q.tests.tasks.result',
schedule_type=Schedule.HOURLY, schedule_type=Schedule.HOURLY,
repeats=1) repeats=1)
+2 -1
View File
@@ -11,7 +11,8 @@ Successful tasks
---------------- ----------------
Shows all successfully executed tasks. Meaning they did not encounter any errors during execution. Shows all successfully executed tasks. Meaning they did not encounter any errors during execution.
From here you can look at details of each task or delete them. From here you can look at details of each task or delete them. Use the group column to sort your results by schedule name or group id.
The table is searchable by `name`, `func` and `group`
Uses the :class:`Success` proxy model. Uses the :class:`Success` proxy model.
+6 -1
View File
@@ -48,12 +48,13 @@ If you want to schedule regular Django management commands, you can use the :mod
Reference Reference
--------- ---------
.. py:function:: schedule(func, *args, hook=None, schedule_type='O', repeats=-1, next_run=now() , **kwargs) .. py:function:: schedule(func, *args, name=None, hook=None, schedule_type='O', repeats=-1, next_run=now() , **kwargs)
Creates a schedule Creates a schedule
:param str func: the function to schedule. Dotted strings only. :param str func: the function to schedule. Dotted strings only.
:param args: arguments for the scheduled function. :param args: arguments for the scheduled function.
:param str name: An optional name for your schedule.
:param str hook: optional result hook function. Dotted strings only. :param str hook: optional result hook function. Dotted strings only.
:param str schedule_type: (O)nce, (H)ourly, (D)aily, (W)eekly, (M)onthly, (Q)uarterly, (Y)early or :attr:`Schedule.TYPE` :param str schedule_type: (O)nce, (H)ourly, (D)aily, (W)eekly, (M)onthly, (Q)uarterly, (Y)early or :attr:`Schedule.TYPE`
:param int repeats: Number of times to repeat schedule. -1=Always, 0=Never, n =n. :param int repeats: Number of times to repeat schedule. -1=Always, 0=Never, n =n.
@@ -68,6 +69,10 @@ Reference
Primary key Primary key
.. py:attribute:: name
A name for your schedule. Tasks created by this schedule will assume this or the primary key as their group id.
.. py:attribute:: func .. py:attribute:: func
The function to be scheduled The function to be scheduled
+78 -9
View File
@@ -31,6 +31,47 @@ Use :func:`async` from your code to quickly offload tasks to the :class:`Cluster
def print_result(task): def print_result(task):
print(task.result) print(task.result)
Groups
------
You can group together results by passing :func:`async` the optional `group` keyword:
.. code-block:: python
# result group example
from django_q import async, result_group
for i in range(4):
async('math.modf', i, group='modf')
# after the tasks have finished you can get the group results
result = result_group('modf')
print(result)
.. code-block:: python
[(0.0, 0.0), (0.0, 1.0), (0.0, 2.0), (0.0, 3.0)]
Take care to not limit your results database too much and that the group identifier is unique for each run.
Instead of :func:`result_group` you can also use :func:`fetch_group` to return a queryset of :class:`Task` objects.:
.. code-block:: python
# fetch group example
from django_q import fetch_group
# count the number of failures
failure_count = fetch_group('modf').filter(success=False).count()
# or print only the successful results
successes = fetch_group('modf').exclude(success=False)
results = [task.result for task in successes]
print(results)
.. note::
Although :func:`fetch_group` returns a queryset, due to the nature of the PickleField , `Queryset.values` will return a list of encoded results.
Use list comprehension or an iterator instead.
Synchronous testing Synchronous testing
------------------- -------------------
@@ -78,19 +119,19 @@ When you are making individual calls to :func:`async` a lot though, it can help
Reference Reference
--------- ---------
.. py:function:: async(func, *args, hook=None, timeout=None, sync=False, redis=None, **kwargs) .. py:function:: async(func, *args, hook=None, group=None, timeout=None,\
sync=False, redis=None, **kwargs)
Puts a task in the cluster queue Puts a task in the cluster queue
:param func: The task function to execute :param object func: The task function to execute
:param args: The arguments for the task function :param tuple args: The arguments for the task function
:type func: object :param object hook: Optional function to call after execution
:param hook: Optional function to call after execution :param str group: An optional group identifier
:type hook: object
:param int timeout: Overrides global cluster :ref:`timeout`. :param int timeout: Overrides global cluster :ref:`timeout`.
:param bool sync: If set to True, async will simulate a task execution :param bool sync: If set to True, async will simulate a task execution
:param redis: Optional redis connection :param redis: Optional redis connection
:param kwargs: Keyword arguments for the task function :param dict kwargs: Keyword arguments for the task function
:returns: The uuid of the task :returns: The uuid of the task
:rtype: str :rtype: str
@@ -106,13 +147,29 @@ Reference
Returns a previously executed task Returns a previously executed task
:param str name: the uuid or name of the task :param str name: the uuid or name of the task
:returns: The task :returns: The task if any
:rtype: Task :rtype: Task
.. versionchanged:: 0.2.0 .. versionchanged:: 0.2.0
Renamed from get_task Renamed from get_task
.. py:function:: result_group(group_id)
Returns the results of a task group
:param str group_id: the group identifier
:returns: a list of results
:rtype: list
.. py:function:: fetch_group(group_id)
Returns a list of tasks in a group
:param str group_id: the group identifier
:returns: a list of Tasks
:rtype: list
.. py:class:: Task .. py:class:: Task
Database model describing an executed task Database model describing an executed task
@@ -174,7 +231,19 @@ Reference
.. py:classmethod:: get_result(task_id) .. py:classmethod:: get_result(task_id)
Get a result directly by task uuid or name Gets a result directly by task uuid or name.
.. py:classmethod:: get_result_group(group_id)
Returns a list of results from a task group.
.. py:classmethod:: get_task(task_id)
Fetches a single task object by uuid or name.
.. py:classmethod:: get_task_group(group_id)
Gets a queryset of tasks with this group id.
.. py:class:: Success .. py:class:: Success