mirror of
https://github.com/django-q2/django-q2.git
synced 2026-10-04 14:38:12 +08:00
Added basic cluster stats
TODO add these to django admin
This commit is contained in:
@@ -4,6 +4,7 @@ Author: Ilan Steemers (koed00@gmail.com
|
|||||||
Github: https://github.com/Koed00/django-q
|
Github: https://github.com/Koed00/django-q
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .main import Cluster, async
|
from .main import Cluster, async, SignedPackage, Stat
|
||||||
|
|
||||||
|
|
||||||
default_app_config = 'django_q.apps.SessionAdminConfig'
|
default_app_config = 'django_q.apps.SessionAdminConfig'
|
||||||
|
|||||||
+99
-13
@@ -1,19 +1,22 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import signal
|
import signal
|
||||||
from multiprocessing import Queue, Event, Process, current_process
|
from multiprocessing import Queue, Event, Process, current_process
|
||||||
import sys
|
import sys
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
# External
|
||||||
import jsonpickle
|
import jsonpickle
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
from django.core.signing import BadSignature
|
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
import redis
|
import redis
|
||||||
|
|
||||||
|
# Django
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# Local
|
||||||
from .conf import LOG_LEVEL, SECRET_KEY, SAVE_LIMIT, WORKERS, COMPRESSED, VERSION
|
from .conf import LOG_LEVEL, SECRET_KEY, SAVE_LIMIT, WORKERS, COMPRESSED, VERSION
|
||||||
from .humanhash import uuid
|
from .humanhash import uuid
|
||||||
from .models import Task, Success
|
from .models import Task, Success
|
||||||
@@ -37,6 +40,7 @@ class Cluster(object):
|
|||||||
return
|
return
|
||||||
self.sentinel = None
|
self.sentinel = None
|
||||||
self.stop_event = None
|
self.stop_event = None
|
||||||
|
self.pid = current_process().pid
|
||||||
signal.signal(signal.SIGTERM, self.sig_handler)
|
signal.signal(signal.SIGTERM, self.sig_handler)
|
||||||
signal.signal(signal.SIGINT, self.sig_handler)
|
signal.signal(signal.SIGINT, self.sig_handler)
|
||||||
|
|
||||||
@@ -51,13 +55,16 @@ class Cluster(object):
|
|||||||
self.stop_event = Event()
|
self.stop_event = Event()
|
||||||
self.sentinel = Process(target=Sentinel, args=(self.stop_event,))
|
self.sentinel = Process(target=Sentinel, args=(self.stop_event,))
|
||||||
self.sentinel.start()
|
self.sentinel.start()
|
||||||
logger.info('Starting Q cluster version {} at {}'.format(VERSION, current_process().pid))
|
logger.info('Starting Q cluster version {} at {}'.format(VERSION, self.pid))
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.stop_event.set()
|
self.stop_event.set()
|
||||||
self.sentinel.join()
|
self.sentinel.join()
|
||||||
logger.info('Q cluster has stopped.')
|
logger.info('Q cluster has stopped.')
|
||||||
|
|
||||||
|
def stats(self):
|
||||||
|
return Stat.load(self.pid)
|
||||||
|
|
||||||
def sig_handler(self, signum, frame):
|
def sig_handler(self, signum, frame):
|
||||||
logger.debug('{} got signal {}'.format(current_process().name, SIGNAL_NAMES.get(signum, 'UNKNOWN')))
|
logger.debug('{} got signal {}'.format(current_process().name, SIGNAL_NAMES.get(signum, 'UNKNOWN')))
|
||||||
self.stop()
|
self.stop()
|
||||||
@@ -67,6 +74,11 @@ class Sentinel(object):
|
|||||||
def __init__(self, event):
|
def __init__(self, event):
|
||||||
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.parent_pid = os.getppid()
|
||||||
|
self.name = current_process().name
|
||||||
|
self.status = 'Initializing'
|
||||||
|
self.tob = timezone.now()
|
||||||
self.stop_event = event
|
self.stop_event = event
|
||||||
self.pool_size = WORKERS
|
self.pool_size = WORKERS
|
||||||
self.pool = []
|
self.pool = []
|
||||||
@@ -118,7 +130,8 @@ class Sentinel(object):
|
|||||||
self.pusher_pid = self.spawn_pusher()
|
self.pusher_pid = self.spawn_pusher()
|
||||||
|
|
||||||
def guard(self):
|
def guard(self):
|
||||||
logger.info('{} checking worker health at {}'.format(current_process().name, current_process().pid))
|
logger.info('{} guarding cluster at {}'.format(current_process().name, self.pid))
|
||||||
|
self.status = 'Running'
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
for p in list(self.pool):
|
for p in list(self.pool):
|
||||||
if not p.is_alive():
|
if not p.is_alive():
|
||||||
@@ -127,10 +140,15 @@ class Sentinel(object):
|
|||||||
self.pool.remove(p)
|
self.pool.remove(p)
|
||||||
# Replace it with a fresh one
|
# Replace it with a fresh one
|
||||||
self.reincarnate(p.pid)
|
self.reincarnate(p.pid)
|
||||||
|
self.stats()
|
||||||
sleep(1)
|
sleep(1)
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
def stats(self):
|
||||||
|
Stat(self).save()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
self.status = 'Stopping'
|
||||||
# Send the STOP signal to the pool
|
# Send the STOP signal to the pool
|
||||||
name = current_process().name
|
name = current_process().name
|
||||||
logger.info('{} stopping pool processes'.format(name))
|
logger.info('{} stopping pool processes'.format(name))
|
||||||
@@ -148,6 +166,7 @@ class Sentinel(object):
|
|||||||
# Finally stop the monitor
|
# Finally stop the monitor
|
||||||
self.done_queue.put('STOP')
|
self.done_queue.put('STOP')
|
||||||
self.pool = []
|
self.pool = []
|
||||||
|
self.status = 'Stopped'
|
||||||
|
|
||||||
|
|
||||||
def pusher(task_queue, e):
|
def pusher(task_queue, e):
|
||||||
@@ -179,11 +198,11 @@ def worker(task_queue, done_queue):
|
|||||||
for pack in iter(task_queue.get, 'STOP'):
|
for pack in iter(task_queue.get, 'STOP'):
|
||||||
# unpickle the task
|
# unpickle the task
|
||||||
try:
|
try:
|
||||||
task = signing.loads(pack, key=SECRET_KEY, salt='django_q.q', serializer=JSONPickleSerializer)
|
task = SignedPackage.loads(pack)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
continue
|
continue
|
||||||
except 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()
|
task['stopped'] = timezone.now()
|
||||||
task['result'] = e
|
task['result'] = e
|
||||||
@@ -226,19 +245,36 @@ def async(func, *args, hook=None, **kwargs):
|
|||||||
Schedules a task
|
Schedules a task
|
||||||
"""
|
"""
|
||||||
task = {'name': uuid()[0], 'func': func, 'hook': hook, 'args': args, 'kwargs': kwargs, 'started': timezone.now()}
|
task = {'name': uuid()[0], 'func': func, 'hook': hook, 'args': args, 'kwargs': kwargs, 'started': timezone.now()}
|
||||||
pack = signing.dumps(task,
|
pack = SignedPackage.dumps(task)
|
||||||
key=SECRET_KEY,
|
|
||||||
salt='django_q.q',
|
|
||||||
compress=COMPRESSED,
|
|
||||||
serializer=JSONPickleSerializer)
|
|
||||||
r.rpush(q_list, pack)
|
r.rpush(q_list, pack)
|
||||||
logger.debug('Pushed {}'.format(pack))
|
logger.debug('Pushed {}'.format(pack))
|
||||||
return task['name']
|
return task['name']
|
||||||
|
|
||||||
|
|
||||||
|
class SignedPackage(object):
|
||||||
|
"""
|
||||||
|
Wraps Django's signing module with custom JsonPickle serializer
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dumps(obj, compressed=COMPRESSED):
|
||||||
|
return signing.dumps(obj,
|
||||||
|
key=SECRET_KEY,
|
||||||
|
salt='django_q.q',
|
||||||
|
compress=compressed,
|
||||||
|
serializer=JSONPickleSerializer)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def loads(obj):
|
||||||
|
return signing.loads(obj,
|
||||||
|
key=SECRET_KEY,
|
||||||
|
salt='django_q.q',
|
||||||
|
serializer=JSONPickleSerializer)
|
||||||
|
|
||||||
|
|
||||||
class JSONPickleSerializer(object):
|
class JSONPickleSerializer(object):
|
||||||
"""
|
"""
|
||||||
Simple wrapper around jsonpickle to be used in signing.dumps and
|
Simple wrapper around JsonPickle for signing.dumps and
|
||||||
signing.loads.
|
signing.loads.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -249,3 +285,53 @@ class JSONPickleSerializer(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def loads(data):
|
def loads(data):
|
||||||
return jsonpickle.loads(data.decode('latin-1'))
|
return jsonpickle.loads(data.decode('latin-1'))
|
||||||
|
|
||||||
|
|
||||||
|
class Stat(object):
|
||||||
|
def __init__(self, sentinel):
|
||||||
|
self.cluster_id = sentinel.parent_pid
|
||||||
|
self.status = sentinel.status
|
||||||
|
self.sentinel = sentinel.pid
|
||||||
|
self.monitor = sentinel.monitor_pid
|
||||||
|
self.pusher = sentinel.pusher_pid
|
||||||
|
self.workers = []
|
||||||
|
for w in sentinel.pool:
|
||||||
|
self.workers.append(w.pid)
|
||||||
|
self.task_q_size = sentinel.task_queue.qsize()
|
||||||
|
self.done_q_size = sentinel.done_queue.qsize()
|
||||||
|
self.tob = sentinel.tob
|
||||||
|
self.timestamp = timezone.now()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key(self):
|
||||||
|
return self.get_key(self.cluster_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_key(cluster_id):
|
||||||
|
return '{}:cluster:{}'.format(prefix, cluster_id)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
r.set(self.key, SignedPackage.dumps(self, True), 5)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(cluster_id=None):
|
||||||
|
if not cluster_id:
|
||||||
|
stats = []
|
||||||
|
keys = r.keys(pattern='{}:cluster:*'.format(prefix))
|
||||||
|
if not keys:
|
||||||
|
return None
|
||||||
|
packs = r.mget(keys)
|
||||||
|
for pack in packs:
|
||||||
|
try:
|
||||||
|
stats.append(SignedPackage.loads(pack))
|
||||||
|
except signing.BadSignature:
|
||||||
|
continue
|
||||||
|
return stats
|
||||||
|
else:
|
||||||
|
pack = r.get(Stat.get_key(cluster_id))
|
||||||
|
if pack:
|
||||||
|
try:
|
||||||
|
return SignedPackage.loads(pack)
|
||||||
|
except signing.BadSignature:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Task',
|
name='Task',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(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)),
|
||||||
@@ -31,6 +31,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
'verbose_name': 'Failed task',
|
||||||
'proxy': True,
|
'proxy': True,
|
||||||
},
|
},
|
||||||
bases=('django_q.task',),
|
bases=('django_q.task',),
|
||||||
@@ -40,6 +41,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
'verbose_name': 'Successful task',
|
||||||
'proxy': True,
|
'proxy': True,
|
||||||
},
|
},
|
||||||
bases=('django_q.task',),
|
bases=('django_q.task',),
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from multiprocessing import Event
|
||||||
|
|
||||||
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 + '/../')
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django_q import Cluster
|
from django_q import Cluster
|
||||||
|
from django_q.main import Sentinel
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
@@ -15,8 +17,17 @@ def cluster():
|
|||||||
|
|
||||||
|
|
||||||
def test_cluster_initial(cluster):
|
def test_cluster_initial(cluster):
|
||||||
|
assert cluster.sentinel is None
|
||||||
cluster.start()
|
cluster.start()
|
||||||
assert cluster.sentinel.is_alive() is True
|
assert cluster.sentinel.is_alive() is True
|
||||||
cluster.stop()
|
cluster.stop()
|
||||||
sleep(2)
|
sleep(2)
|
||||||
assert cluster.sentinel.is_alive() is False
|
assert cluster.sentinel.is_alive() is False
|
||||||
|
|
||||||
|
def test_sentinel():
|
||||||
|
event = Event()
|
||||||
|
sentinel = Sentinel(event)
|
||||||
|
sleep(1)
|
||||||
|
assert sentinel.monitor_pid > 0
|
||||||
|
assert sentinel.pusher_pid > 0
|
||||||
|
assert len(sentinel.pool) == sentinel.pool_size + 2
|
||||||
|
|||||||
Reference in New Issue
Block a user