mirror of
https://github.com/django-q2/django-q2.git
synced 2026-10-09 06:18:11 +08:00
Fixes #7
* added a synchronous cluster simulation * added a test for the sync function * moved signing into its own module * refactored imports to solve circular import problems stemming from the task module pretending to be a cluster * added documentation for the new sync option
This commit is contained in:
@@ -1,3 +1,9 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
myPath = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, myPath)
|
||||||
|
|
||||||
from .tasks import async, schedule, result, fetch
|
from .tasks import async, schedule, result, fetch
|
||||||
from .models import Task, Schedule
|
from .models import Task, Schedule
|
||||||
|
|
||||||
|
|||||||
+10
-12
@@ -19,24 +19,22 @@ import ast
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
from multiprocessing import Queue, Event, Process, Value, current_process
|
from multiprocessing import Queue, Event, Process, Value, current_process
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError:
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
# external
|
# external
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.core import signing
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .conf import Conf, redis_client, logger
|
import signing
|
||||||
from .models import Task, Success, Schedule
|
import tasks
|
||||||
from .monitor import Status, Stat
|
|
||||||
from .tasks import SignedPackage, async
|
from django_q.conf import Conf, redis_client, logger
|
||||||
|
from django_q.models import Task, Success, Schedule
|
||||||
|
from django_q.monitor import Status, Stat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cluster(object):
|
class Cluster(object):
|
||||||
@@ -345,7 +343,7 @@ def worker(task_queue, result_queue, timer):
|
|||||||
task_count += 1
|
task_count += 1
|
||||||
# unpickle the task
|
# unpickle the task
|
||||||
try:
|
try:
|
||||||
task = SignedPackage.loads(pack)
|
task = signing.SignedPackage.loads(pack)
|
||||||
except (TypeError, signing.BadSignature) as e:
|
except (TypeError, signing.BadSignature) as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
continue
|
continue
|
||||||
@@ -450,7 +448,7 @@ def scheduler(list_key=Conf.Q_LIST):
|
|||||||
s.repeats = 0
|
s.repeats = 0
|
||||||
# send it to the cluster
|
# send it to the cluster
|
||||||
kwargs['list_key'] = list_key
|
kwargs['list_key'] = list_key
|
||||||
s.task = async(s.func, *args, **kwargs)
|
s.task = tasks.async(s.func, *args, **kwargs)
|
||||||
if not s.task:
|
if not s.task:
|
||||||
logger.error(_('{} failed to create a task from schedule {} [{}]').format(current_process().name, s.id), s.func)
|
logger.error(_('{} failed to create a task from schedule {} [{}]').format(current_process().name, s.id), s.func)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+4
-5
@@ -4,13 +4,12 @@ import socket
|
|||||||
from blessed import Terminal
|
from blessed import Terminal
|
||||||
|
|
||||||
# django
|
# django
|
||||||
from django.core import signing
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
# local
|
# local
|
||||||
|
import signing
|
||||||
from django_q.conf import Conf, redis_client, logger
|
from django_q.conf import Conf, redis_client, logger
|
||||||
from django_q.tasks import SignedPackage
|
|
||||||
|
|
||||||
|
|
||||||
def monitor(run_once=False):
|
def monitor(run_once=False):
|
||||||
@@ -147,7 +146,7 @@ class Stat(Status):
|
|||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
try:
|
try:
|
||||||
self.r.set(self.key, SignedPackage.dumps(self, True), 3)
|
self.r.set(self.key, signing.SignedPackage.dumps(self, True), 3)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
||||||
@@ -165,7 +164,7 @@ class Stat(Status):
|
|||||||
if r.exists(key):
|
if r.exists(key):
|
||||||
pack = r.get(key)
|
pack = r.get(key)
|
||||||
try:
|
try:
|
||||||
return SignedPackage.loads(pack)
|
return signing.SignedPackage.loads(pack)
|
||||||
except signing.BadSignature:
|
except signing.BadSignature:
|
||||||
return None
|
return None
|
||||||
return Status(cluster_id)
|
return Status(cluster_id)
|
||||||
@@ -182,7 +181,7 @@ class Stat(Status):
|
|||||||
packs = r.mget(keys)
|
packs = r.mget(keys)
|
||||||
for pack in packs:
|
for pack in packs:
|
||||||
try:
|
try:
|
||||||
stats.append(SignedPackage.loads(pack))
|
stats.append(signing.SignedPackage.loads(pack))
|
||||||
except signing.BadSignature:
|
except signing.BadSignature:
|
||||||
continue
|
continue
|
||||||
return stats
|
return stats
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
try:
|
||||||
|
import cPickle as pickle
|
||||||
|
except ImportError:
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
from django.core import signing
|
||||||
|
from django_q.conf import Conf
|
||||||
|
|
||||||
|
BadSignature = signing.BadSignature
|
||||||
|
|
||||||
|
class SignedPackage(object):
|
||||||
|
"""
|
||||||
|
Wraps Django's signing module with custom Pickle serializer
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dumps(obj, compressed=Conf.COMPRESSED):
|
||||||
|
return signing.dumps(obj,
|
||||||
|
key=Conf.SECRET_KEY,
|
||||||
|
salt='django_q.q',
|
||||||
|
compress=compressed,
|
||||||
|
serializer=PickleSerializer)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def loads(obj):
|
||||||
|
return signing.loads(obj,
|
||||||
|
key=Conf.SECRET_KEY,
|
||||||
|
salt='django_q.q',
|
||||||
|
serializer=PickleSerializer)
|
||||||
|
|
||||||
|
|
||||||
|
class PickleSerializer(object):
|
||||||
|
"""
|
||||||
|
Simple wrapper around Pickle for signing.dumps and
|
||||||
|
signing.loads.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dumps(obj):
|
||||||
|
return pickle.dumps(obj)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def loads(data):
|
||||||
|
return pickle.loads(data)
|
||||||
+30
-37
@@ -1,30 +1,43 @@
|
|||||||
|
from multiprocessing import Queue, Value
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cPickle as pickle
|
import cPickle as pickle
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
# django
|
# django
|
||||||
from django.core import signing
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
# local
|
# local
|
||||||
from .conf import Conf, redis_client, logger
|
import signing
|
||||||
from .models import Schedule, Task
|
import cluster
|
||||||
from .humanhash import uuid
|
from django_q.conf import Conf, redis_client, logger
|
||||||
|
from django_q.models import Schedule, Task
|
||||||
|
from django_q.humanhash import uuid
|
||||||
|
|
||||||
|
|
||||||
def async(func, *args, **kwargs):
|
def async(func, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sends a task to the cluster
|
Sends a task to the cluster
|
||||||
"""
|
"""
|
||||||
|
# optional hook
|
||||||
hook = kwargs.pop('hook', None)
|
hook = kwargs.pop('hook', None)
|
||||||
|
# optional list_key
|
||||||
list_key = kwargs.pop('list_key', Conf.Q_LIST)
|
list_key = kwargs.pop('list_key', Conf.Q_LIST)
|
||||||
|
# optional redis connection
|
||||||
r = kwargs.pop('redis', redis_client)
|
r = kwargs.pop('redis', redis_client)
|
||||||
|
# optional sync mode
|
||||||
|
s = kwargs.pop('sync', False)
|
||||||
|
# get an id
|
||||||
tag = uuid()
|
tag = uuid()
|
||||||
|
# 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, 'hook': hook, 'args': args, 'kwargs': kwargs,
|
||||||
'started': timezone.now()}
|
'started': timezone.now()}
|
||||||
pack = SignedPackage.dumps(task)
|
# sign it
|
||||||
|
pack = signing.SignedPackage.dumps(task)
|
||||||
|
if s:
|
||||||
|
return _sync(task['id'], pack)
|
||||||
|
# push it
|
||||||
r.rpush(list_key, pack)
|
r.rpush(list_key, pack)
|
||||||
logger.debug('Pushed {}'.format(tag))
|
logger.debug('Pushed {}'.format(tag))
|
||||||
return task['id']
|
return task['id']
|
||||||
@@ -81,37 +94,17 @@ def fetch(task_id):
|
|||||||
return Task.get_task(task_id)
|
return Task.get_task(task_id)
|
||||||
|
|
||||||
|
|
||||||
class SignedPackage(object):
|
def _sync(task_id, pack):
|
||||||
"""
|
"""
|
||||||
Wraps Django's signing module with custom Pickle serializer
|
Simulates a package travelling through the cluster.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
task_queue = Queue()
|
||||||
|
result_queue = Queue()
|
||||||
|
task_queue.put(pack)
|
||||||
|
task_queue.put('STOP')
|
||||||
|
cluster.worker(task_queue, result_queue, Value('b', -1))
|
||||||
|
result_queue.put('STOP')
|
||||||
|
cluster.monitor(result_queue)
|
||||||
|
return task_id
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def dumps(obj, compressed=Conf.COMPRESSED):
|
|
||||||
return signing.dumps(obj,
|
|
||||||
key=Conf.SECRET_KEY,
|
|
||||||
salt='django_q.q',
|
|
||||||
compress=compressed,
|
|
||||||
serializer=PickleSerializer)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def loads(obj):
|
|
||||||
return signing.loads(obj,
|
|
||||||
key=Conf.SECRET_KEY,
|
|
||||||
salt='django_q.q',
|
|
||||||
serializer=PickleSerializer)
|
|
||||||
|
|
||||||
|
|
||||||
class PickleSerializer(object):
|
|
||||||
"""
|
|
||||||
Simple wrapper around Pickle for signing.dumps and
|
|
||||||
signing.loads.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def dumps(obj):
|
|
||||||
return pickle.dumps(obj)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def loads(data):
|
|
||||||
return pickle.loads(data)
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ def r():
|
|||||||
def test_redis_connection(r):
|
def test_redis_connection(r):
|
||||||
assert r.ping() is True
|
assert r.ping() is True
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_sync(r):
|
||||||
|
task = async('django_q.tests.tasks.count_letters', DEFAULT_WORDLIST, redis=r, sync=True)
|
||||||
|
assert result(task) == 1506
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_cluster_initial(r):
|
def test_cluster_initial(r):
|
||||||
|
|||||||
+26
-2
@@ -1,7 +1,10 @@
|
|||||||
Tasks
|
Tasks
|
||||||
=====
|
=====
|
||||||
|
|
||||||
Use :py:func:`async` from your code to quickly offload tasks to the :mod:`cluster`:
|
Async
|
||||||
|
-----
|
||||||
|
|
||||||
|
Use :func:`async` from your code to quickly offload tasks to the :mod:`cluster`:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
@@ -27,6 +30,26 @@ Use :py:func:`async` from your code to quickly offload tasks to the :mod:`clust
|
|||||||
def print_result(task):
|
def print_result(task):
|
||||||
print(task.result)
|
print(task.result)
|
||||||
|
|
||||||
|
Synchronous testing
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
:func:`async` can be instructed to execute a task immediately by setting the optional keyword `sync=True`.
|
||||||
|
The task will then be injected straight into a worker and the result saved by a monitor instance::
|
||||||
|
|
||||||
|
from django_q import async, fetch
|
||||||
|
|
||||||
|
# create a synchronous task
|
||||||
|
task_id = async('my.buggy.code', sync=True)
|
||||||
|
|
||||||
|
# the task will then be available immediately
|
||||||
|
task = fetch(task_id)
|
||||||
|
|
||||||
|
# and can be examined
|
||||||
|
if not task.success:
|
||||||
|
print('An error occurred: {}'.format(task.result))
|
||||||
|
|
||||||
|
Note that :func:`async` will block until the task is executed and saved. This feature is intended for debugging and development.
|
||||||
|
|
||||||
Connection pooling
|
Connection pooling
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
@@ -50,7 +73,7 @@ When you are making individual calls to :func:`async` a lot though, it can help
|
|||||||
Reference
|
Reference
|
||||||
---------
|
---------
|
||||||
|
|
||||||
.. py:function:: async(func, *args, hook=None, redis=None, **kwargs)
|
.. py:function:: async(func, *args, hook=None, sync=False, redis=None, **kwargs)
|
||||||
|
|
||||||
Puts a task in the cluster queue
|
Puts a task in the cluster queue
|
||||||
|
|
||||||
@@ -59,6 +82,7 @@ Reference
|
|||||||
:type func: str or object
|
:type func: str or object
|
||||||
:param hook: Optional function to call after execution
|
:param hook: Optional function to call after execution
|
||||||
:type hook: str or object
|
:type hook: str or object
|
||||||
|
: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 kwargs: Keyword arguments for the task function
|
||||||
:returns: The uuid of the task
|
:returns: The uuid of the task
|
||||||
|
|||||||
Reference in New Issue
Block a user