* updated README

* both func and hook check for callable.
This commit is contained in:
Ilan Steemers
2015-06-26 14:24:25 +02:00
parent 28d6c26256
commit 666bdc97dc
7 changed files with 158 additions and 46 deletions
+34 -1
View File
@@ -16,8 +16,41 @@ Schedule the asynchronous execution of a function by calling `async` from within
```python ```python
async(func,*args,hook=None,**kwargs) async(func,*args,hook=None,**kwargs)
``` ```
The optional hook function gets the finished task object as its first argument after execution ####Basic example
```python
from django_q import async
# math.copysign(2,-2)
async('math.copysign', 2, -2)
# also
from math import copysign
async(copysign, 2, -2)
```
#### Result example
```python
from django_q import async, result
# create the task
task_id = async('math.copysign', 2, -2)
# get the result
task_result = result(task_id)
# result returns None if the task has not been executed yet
# so it makes more sense to use a hook:
from hooks import print_result
async('math.modf', 2.5, hook='hooks.print_result')
# hooks.py
def print_result(task):
print(task.result)
```
### Management commands ### Management commands
#### `qcluster` #### `qcluster`
+37 -2
View File
@@ -25,8 +25,43 @@ from within your Django project.
async(func,*args,hook=None,**kwargs) async(func,*args,hook=None,**kwargs)
The optional hook function gets the finished task object as its first Basic example
argument after execution ^^^^^^^^^^^^^
.. code:: python
from django_q import async
# math.copysign(2,-2)
async('math.copysign', 2, -2)
# also
from math import copysign
async(copysign, 2, -2)
Result example
^^^^^^^^^^^^^^
.. code:: python
from django_q import async, result
# create the task
task_id = async('math.copysign', 2, -2)
# get the result
task_result = result(task_id)
# result returns None if the task has not been executed yet
# so it makes more sense to use a hook:
from hooks import print_result
async('math.modf', 2.5, hook='hooks.print_result')
# hooks.py
def print_result(task):
print(task.result)
Management commands Management commands
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
+42 -23
View File
@@ -132,7 +132,8 @@ class Cluster(object):
class Sentinel(object): class Sentinel(object):
def __init__(self, stop_event, start_event, list_key=Q_LIST): def __init__(self, stop_event, start_event, list_key=Q_LIST, start=True):
# Make sure we catch signals for the pool
signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal.SIG_DFL) signal.signal(signal.SIGTERM, signal.SIG_DFL)
self.pid = current_process().pid self.pid = current_process().pid
@@ -151,8 +152,9 @@ class Sentinel(object):
self.event_out = Event() self.event_out = Event()
self.monitor_pid = None self.monitor_pid = None
self.pusher_pid = None self.pusher_pid = None
self.spawn_cluster() if start:
self.guard() self.spawn_cluster()
self.guard()
def spawn_process(self, target, *args): def spawn_process(self, target, *args):
# This is just for PyCharm to not crash. Ignore it. # This is just for PyCharm to not crash. Ignore it.
@@ -210,8 +212,9 @@ class Sentinel(object):
Stat(self).save() Stat(self).save()
if self.stop_event.is_set(): if self.stop_event.is_set():
break break
# Call scheduler once a minute (or so)
counter += 1 counter += 1
if counter > 15: if counter > 30:
counter = 0 counter = 0
scheduler() scheduler()
sleep(2) sleep(2)
@@ -269,6 +272,14 @@ def monitor(done_queue):
def worker(task_queue, done_queue): def worker(task_queue, done_queue):
name = current_process().name name = current_process().name
logger.info('{} ready for work at {}'.format(name, current_process().pid)) logger.info('{} ready for work at {}'.format(name, current_process().pid))
task = {}
def return_pack(res, success):
task['result'] = res
task['stopped'] = timezone.now()
task['success'] = success
done_queue.put(task)
for pack in iter(task_queue.get, 'STOP'): for pack in iter(task_queue.get, 'STOP'):
# unpickle the task # unpickle the task
try: try:
@@ -278,30 +289,33 @@ def worker(task_queue, done_queue):
continue continue
except signing.BadSignature as e: except signing.BadSignature as e:
task['name'] = task['name'].rsplit(":", 1)[0] task['name'] = task['name'].rsplit(":", 1)[0]
task['stopped'] = timezone.now() return_pack(e, False)
task['result'] = e
task['success'] = False
done_queue.put(task)
continue continue
module, func = task['func'].rsplit('.', 1)
logger.info('{} processing [{}]'.format(name, task['name'])) logger.info('{} processing [{}]'.format(name, task['name']))
f = task['func']
# if it's not an instance try to get it from the string
if not callable(task['func']):
try:
module, func = f.rsplit('.', 1)
m = importlib.import_module(module)
f = getattr(m, func)
except (ValueError, ImportError, AttributeError) as e:
logger.error(e)
return_pack(e, False)
continue
# execute the payload
try: try:
m = importlib.import_module(module) result = f(*task['args'], **task['kwargs'])
f = getattr(m, func) return_pack(result, True)
task['result'] = f(*task['args'], **task['kwargs'])
task['stopped'] = timezone.now()
task['success'] = True
done_queue.put(task)
gc.collect()
except Exception as e: except Exception as e:
task['result'] = e return_pack(e, False)
task['stopped'] = timezone.now()
task['success'] = False
done_queue.put(task)
logger.info('{} stopped doing work'.format(name)) logger.info('{} stopped doing work'.format(name))
def save_task(task): def save_task(task):
"""
Saves the task package to Django
"""
if task['success'] and 0 < SAVE_LIMIT < Success.objects.count(): if task['success'] and 0 < SAVE_LIMIT < Success.objects.count():
Success.objects.first().delete() Success.objects.first().delete()
Task.objects.create(name=task['name'], Task.objects.create(name=task['name'],
@@ -451,19 +465,22 @@ class Stat(Status):
def scheduler(): def scheduler():
for schedule in Schedule.objects.exclude(repeats=0).filter(next_run__lt=timezone.now()): for schedule in Schedule.objects.exclude(repeats=0).filter(next_run__lt=timezone.now()):
args = () args = ()
kwargs= {} kwargs = {}
# get args, kwargs and hook
if schedule.kwargs: if schedule.kwargs:
try: try:
# eval should be safe here cause dict()
kwargs = eval('dict({})'.format(schedule.kwargs)) kwargs = eval('dict({})'.format(schedule.kwargs))
except SyntaxError: except SyntaxError:
kwargs={} kwargs = {}
if schedule.args: if schedule.args:
args = ast.literal_eval(schedule.args) args = ast.literal_eval(schedule.args)
# single value won't eval to tuple, so:
if type(args) != tuple: if type(args) != tuple:
args = (args,) args = (args,)
if schedule.hook: if schedule.hook:
kwargs['hook'] = schedule.hook kwargs['hook'] = schedule.hook
schedule.task = async(schedule.func, *args, **kwargs) # set up the next run time
if not schedule.schedule_type == schedule.ONCE: if not schedule.schedule_type == schedule.ONCE:
next_run = arrow.get(schedule.next_run) next_run = arrow.get(schedule.next_run)
if schedule.schedule_type == schedule.HOURLY: if schedule.schedule_type == schedule.HOURLY:
@@ -482,6 +499,8 @@ def scheduler():
schedule.repeats += -1 schedule.repeats += -1
else: else:
schedule.repeats = 0 schedule.repeats = 0
# send it to the cluster
schedule.task = async(schedule.func, *args, **kwargs)
if not schedule.task: if not schedule.task:
logger.error('{} failed to create task from schedule {}').format(current_process().name, schedule.id) logger.error('{} failed to create task from schedule {}').format(current_process().name, schedule.id)
else: else:
+11 -11
View File
@@ -15,26 +15,26 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Schedule', name='Schedule',
fields=[ fields=[
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
('func', models.CharField(max_length=256)), ('func', models.CharField(help_text='e.g. module.tasks.function', max_length=256)),
('hook', models.CharField(blank=True, max_length=256, null=True)), ('hook', models.CharField(blank=True, help_text='e.g. module.tasks.result_function', max_length=256, null=True)),
('args', models.CharField(blank=True, max_length=256, null=True)), ('args', models.CharField(blank=True, help_text="e.g. 1, 2, 'John'", max_length=256, null=True)),
('kwargs', models.CharField(blank=True, max_length=256, null=True)), ('kwargs', models.CharField(blank=True, help_text="e.g. x=1, y=2, name='John'", 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')), ('schedule_type', models.CharField(choices=[('O', 'Once'), ('H', 'Hourly'), ('D', 'Daily'), ('W', 'Weekly'), ('M', 'Monthly'), ('Q', 'Quarterly'), ('Y', 'Yearly')], default='O', verbose_name='Schedule Type', max_length=1)),
('repeats', models.SmallIntegerField(verbose_name='Repeats', default=-1)), ('repeats', models.SmallIntegerField(help_text='n = n times, -1 = forever', default=-1, verbose_name='Repeats')),
('next_run', models.DateTimeField(null=True, verbose_name='Next Run', default=django.utils.timezone.now)), ('next_run', models.DateTimeField(default=django.utils.timezone.now, null=True, verbose_name='Next Run')),
('task', models.CharField(editable=False, max_length=100, null=True)), ('task', models.CharField(editable=False, max_length=100, null=True)),
], ],
options={ options={
'ordering': ['next_run'],
'verbose_name': 'Scheduled task', 'verbose_name': 'Scheduled task',
'ordering': ['next_run'],
}, },
), ),
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, auto_created=True, primary_key=True, verbose_name='ID')),
('name', models.CharField(editable=False, 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', picklefield.fields.PickledObjectField(editable=False)), ('args', picklefield.fields.PickledObjectField(editable=False)),
+10 -5
View File
@@ -37,14 +37,19 @@ class Task(models.Model):
@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:
module, func = instance.hook.rsplit('.', 1) logger = logging.getLogger('django-q')
f = instance.hook
if not callable(f):
try:
module, func = f.rsplit('.', 1)
m = importlib.import_module(module)
f = getattr(m, func)
except (ValueError, ImportError, AttributeError):
logger.error(_('malformed return hook \'{}\' for {}').format(instance.hook, instance.name))
try: try:
m = importlib.import_module(module)
f = getattr(m, func)
f(instance) f(instance)
except Exception as e: except Exception as e:
logger = logging.getLogger('django-q') logger.error(_('return hook {} failed on {}').format(instance.hook, instance.name))
logger.error(_('return hook failed on {}').format(instance.name))
logger.exception(e) logger.exception(e)
+23 -3
View File
@@ -11,6 +11,7 @@ sys.path.insert(0, myPath + '/../')
from django_q.core import Cluster, r, async, pusher, worker, monitor, Sentinel from django_q.core import Cluster, r, async, pusher, worker, monitor, Sentinel
from django_q.humanhash import DEFAULT_WORDLIST from django_q.humanhash import DEFAULT_WORDLIST
from django_q import result, get_task from django_q import result, get_task
from django_q.tests.tasks import multiply
class WordClass(object): class WordClass(object):
@@ -20,6 +21,7 @@ class WordClass(object):
def get_words(self): def get_words(self):
return self.word_list return self.word_list
def test_redis_connection(): def test_redis_connection():
assert r.ping() is True assert r.ping() is True
@@ -43,6 +45,7 @@ def test_sentinel():
Sentinel(stop_event, start_event, list_key='sentinel_test:q') Sentinel(stop_event, start_event, list_key='sentinel_test:q')
assert start_event.is_set() assert start_event.is_set()
@pytest.mark.django_db @pytest.mark.django_db
def test_cluster(): def test_cluster():
list_key = 'cluster_test:q' list_key = 'cluster_test:q'
@@ -72,6 +75,7 @@ def test_cluster():
assert result(task) == 1506 assert result(task) == 1506
r.delete(list_key) r.delete(list_key)
@pytest.mark.django_db @pytest.mark.django_db
def run_cluster(): def run_cluster():
list_key = 'run_test:q' list_key = 'run_test:q'
@@ -83,6 +87,7 @@ def run_cluster():
assert c.stop() is True assert c.stop() is True
r.delete(list_key) r.delete(list_key)
@pytest.mark.django_db @pytest.mark.django_db
def blah_async(): def blah_async():
a = async('django_q.tests.tasks.count_letters', DEFAULT_WORDLIST, hook='django_q.tests.test_q.assert_result') a = async('django_q.tests.tasks.count_letters', DEFAULT_WORDLIST, hook='django_q.tests.test_q.assert_result')
@@ -91,35 +96,50 @@ def blah_async():
c = async('django_q.tests.tasks.count_letters', DEFAULT_WORDLIST, 'oneargumentoomany', c = async('django_q.tests.tasks.count_letters', DEFAULT_WORDLIST, 'oneargumentoomany',
hook='django_q.tests.test_q.assert_bad_result') hook='django_q.tests.test_q.assert_bad_result')
# unknown function # unknown function
d = async('django_q.tests.tasks.does_not_exist', WordClass(), hook='django_q.tests.test_q.assert_bad_result') d = async('django_q.tests.tasks.does_not_exist', WordClass(), hook='django_q.tests.test_q.assert_bad_result')
# function without result # function without result
e = async('django_q.tests.tasks.countdown', 100000) e = async('django_q.tests.tasks.countdown', 100000)
# function as instance
f = async(multiply, 753, 2, hook=assert_result)
# check if everything has a task name
assert isinstance(a, str) assert isinstance(a, str)
assert isinstance(b, str) assert isinstance(b, str)
assert isinstance(c, str) assert isinstance(c, str)
assert isinstance(d, str) assert isinstance(d, str)
assert isinstance(e, str) assert isinstance(e, str)
assert isinstance(f, str)
# run the cluster to execute the tasks
run_cluster() run_cluster()
# task a
result_a = get_task(a) result_a = get_task(a)
assert result_a is not None assert result_a is not None
assert result_a.success is True assert result_a.success is True
assert result(a) == 1506 assert result(a) == 1506
# task b
result_b = get_task(b) result_b = get_task(b)
assert result_b is not None assert result_b is not None
assert result_b.success is True assert result_b.success is True
assert result(b) == 1506 assert result(b) == 1506
# task c
result_c = get_task(c) result_c = get_task(c)
assert result_c is not None assert result_c is not None
assert result_c.success is False assert result_c.success is False
# task d
result_d = get_task(d) result_d = get_task(d)
assert result_d is not None assert result_d is not None
assert result_d.success is False assert result_d.success is False
# task e
result_e = get_task(e) result_e = get_task(e)
assert result_e is not None assert result_e is not None
assert result_e.success is True assert result_e.success is True
assert result(b) is None assert result(e) is None
# task f
result_f = get_task(f)
assert result_f is not None
assert result_f.success is True
assert result(f) == 1506
# not sure if this actually asserts, but it is called
@pytest.mark.django_db @pytest.mark.django_db
def assert_result(task): def assert_result(task):
assert task is not None assert task is not None
+1 -1
View File
@@ -1,4 +1,4 @@
arrow arrow==0.5.4
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